From 5fd2aaa689e65c37b1ce23b5f8d129aea225270e Mon Sep 17 00:00:00 2001 From: feie9456 Date: Tue, 24 Jun 2025 10:56:21 +0800 Subject: [PATCH] first commit --- .github/copilot-instructions.md | 42 + .vscode/tasks.json | 22 + README.md | 217 +++- bun.lock | 220 ++++ create_library.sql | 239 ++++ drizzle.config.ts | 14 + example_data.sql | 239 ++++ next.config.ts | 7 +- package.json | 18 +- sql/admin_query.sql | 469 ++++++++ sql/advance_query.sql | 30 + sql/create_library.sql | 346 ++++++ sql/example_data.sql | 208 ++++ sql/requirements.md | 42 + sql/user_query.sql | 340 ++++++ src/app/admin/books/[id]/edit/page.tsx | 360 ++++++ src/app/admin/books/[id]/page.tsx | 195 ++++ src/app/admin/books/add/page.tsx | 314 +++++ src/app/admin/books/page.tsx | 323 +++++ src/app/admin/borrows/page.tsx | 298 +++++ src/app/admin/fines/page.tsx | 339 ++++++ src/app/admin/page.tsx | 238 ++++ src/app/admin/students/[id]/edit/page.tsx | 328 ++++++ src/app/admin/students/[id]/page.tsx | 240 ++++ src/app/admin/students/add/page.tsx | 299 +++++ src/app/admin/students/page.tsx | 308 +++++ src/app/api/books/[id]/route.ts | 153 +++ src/app/api/books/route.ts | 114 ++ src/app/api/borrow/renew/route.ts | 90 ++ src/app/api/borrow/return/route.ts | 112 ++ src/app/api/borrow/route.ts | 187 +++ src/app/api/students/[id]/route.ts | 101 ++ src/app/api/students/route.ts | 149 +++ src/app/api/test-db/route.ts | 36 + src/app/books/page.tsx | 284 +++++ src/app/layout.tsx | 27 +- src/app/page.new.tsx | 82 ++ src/app/page.tsx | 171 ++- src/app/student/history/page.tsx | 298 +++++ src/app/student/page.tsx | 304 +++++ src/components/providers.tsx | 26 + src/lib/db/index.ts | 16 + src/lib/db/schema.new.ts | 215 ++++ src/lib/db/schema.old.ts | 224 ++++ src/lib/db/schema.ts | 211 ++++ src/lib/types/index.ts | 112 ++ src/lib/utils/index.ts | 6 + .../image-20250621142515480.png | Bin 0 -> 191909 bytes .../image-20250621142527092.png | Bin 0 -> 191909 bytes .../image-20250621142536941.png | Bin 0 -> 74117 bytes .../image-20250621142552089.png | Bin 0 -> 128902 bytes .../image-20250621142558447.png | Bin 0 -> 70192 bytes .../image-20250621142605643.png | Bin 0 -> 35713 bytes .../image-20250621142629990.png | Bin 0 -> 35713 bytes .../image-20250621143046826.png | Bin 0 -> 279577 bytes .../image-20250621143212018.png | Bin 0 -> 197140 bytes .../image-20250621143310581.png | Bin 0 -> 172514 bytes .../image-20250621143344465.png | Bin 0 -> 137226 bytes .../image-20250621143408899.png | Bin 0 -> 188629 bytes .../image-20250624105000930.png | Bin 0 -> 717924 bytes .../image-20250624105028874.png | Bin 0 -> 416103 bytes .../image-20250624105126923.png | Bin 0 -> 369677 bytes .../image-20250624105136013.png | Bin 0 -> 203993 bytes .../image-20250624105225708.png | Bin 0 -> 308132 bytes .../image-20250624105258938.png | Bin 0 -> 270100 bytes 智能图书管理数据库应用系统.md | 1037 +++++++++++++++++ 66 files changed, 9515 insertions(+), 135 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 .vscode/tasks.json create mode 100644 create_library.sql create mode 100644 drizzle.config.ts create mode 100644 example_data.sql create mode 100644 sql/admin_query.sql create mode 100644 sql/advance_query.sql create mode 100644 sql/create_library.sql create mode 100644 sql/example_data.sql create mode 100644 sql/requirements.md create mode 100644 sql/user_query.sql create mode 100644 src/app/admin/books/[id]/edit/page.tsx create mode 100644 src/app/admin/books/[id]/page.tsx create mode 100644 src/app/admin/books/add/page.tsx create mode 100644 src/app/admin/books/page.tsx create mode 100644 src/app/admin/borrows/page.tsx create mode 100644 src/app/admin/fines/page.tsx create mode 100644 src/app/admin/page.tsx create mode 100644 src/app/admin/students/[id]/edit/page.tsx create mode 100644 src/app/admin/students/[id]/page.tsx create mode 100644 src/app/admin/students/add/page.tsx create mode 100644 src/app/admin/students/page.tsx create mode 100644 src/app/api/books/[id]/route.ts create mode 100644 src/app/api/books/route.ts create mode 100644 src/app/api/borrow/renew/route.ts create mode 100644 src/app/api/borrow/return/route.ts create mode 100644 src/app/api/borrow/route.ts create mode 100644 src/app/api/students/[id]/route.ts create mode 100644 src/app/api/students/route.ts create mode 100644 src/app/api/test-db/route.ts create mode 100644 src/app/books/page.tsx create mode 100644 src/app/page.new.tsx create mode 100644 src/app/student/history/page.tsx create mode 100644 src/app/student/page.tsx create mode 100644 src/components/providers.tsx create mode 100644 src/lib/db/index.ts create mode 100644 src/lib/db/schema.new.ts create mode 100644 src/lib/db/schema.old.ts create mode 100644 src/lib/db/schema.ts create mode 100644 src/lib/types/index.ts create mode 100644 src/lib/utils/index.ts create mode 100644 智能图书管理数据库应用系统.assets/image-20250621142515480.png create mode 100644 智能图书管理数据库应用系统.assets/image-20250621142527092.png create mode 100644 智能图书管理数据库应用系统.assets/image-20250621142536941.png create mode 100644 智能图书管理数据库应用系统.assets/image-20250621142552089.png create mode 100644 智能图书管理数据库应用系统.assets/image-20250621142558447.png create mode 100644 智能图书管理数据库应用系统.assets/image-20250621142605643.png create mode 100644 智能图书管理数据库应用系统.assets/image-20250621142629990.png create mode 100644 智能图书管理数据库应用系统.assets/image-20250621143046826.png create mode 100644 智能图书管理数据库应用系统.assets/image-20250621143212018.png create mode 100644 智能图书管理数据库应用系统.assets/image-20250621143310581.png create mode 100644 智能图书管理数据库应用系统.assets/image-20250621143344465.png create mode 100644 智能图书管理数据库应用系统.assets/image-20250621143408899.png create mode 100644 智能图书管理数据库应用系统.assets/image-20250624105000930.png create mode 100644 智能图书管理数据库应用系统.assets/image-20250624105028874.png create mode 100644 智能图书管理数据库应用系统.assets/image-20250624105126923.png create mode 100644 智能图书管理数据库应用系统.assets/image-20250624105136013.png create mode 100644 智能图书管理数据库应用系统.assets/image-20250624105225708.png create mode 100644 智能图书管理数据库应用系统.assets/image-20250624105258938.png create mode 100644 智能图书管理数据库应用系统.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..fcb34ee --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,42 @@ +# Copilot Instructions + + + +## Project Overview +This is an intelligent library management system built with Next.js, TypeScript, and PostgreSQL. The system supports both admin and student interfaces with comprehensive CRUD operations. + +## Database Information +- Database: PostgreSQL (127.0.0.1:5432) +- Username: feie9454 +- Password: (empty) +- Schema: library + +## Code Style Guidelines +- Use TypeScript for all files +- Use Tailwind CSS for styling +- Follow Next.js App Router conventions +- Use meaningful variable and function names +- Add proper error handling and validation +- Use server components where appropriate +- Implement proper authentication and authorization + +## Key Features to Implement +1. Book Management (CRUD operations, batch import) +2. Student Account Management +3. Borrowing/Returning/Renewal System +4. Reservation System +5. Fine Management +6. Book Reviews and Recommendations +7. Admin Dashboard +8. Student Portal + +## Database Schema +The system uses tables: books, students, admins, borrow_records, reservations, fines, reviews, system_settings +Refer to the SQL files for complete schema definition. + +## Architecture Patterns +- Use API routes for database operations +- Implement proper data validation +- Use React Query/SWR for data fetching +- Separate business logic from UI components +- Use environment variables for configuration diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..676022a --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,22 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "启动开发服务器", + "type": "shell", + "command": "bun", + "args": [ + "dev" + ], + "group": "build", + "isBackground": true, + "problemMatcher": [], + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "new" + } + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index e215bc4..b891103 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,213 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# 智能图书馆管理系统 -## Getting Started +基于Next.js、TypeScript和PostgreSQL构建的现代化图书馆管理系统,支持管理员和学生双界面操作。 -First, run the development server: +## 🚀 功能特性 +### 管理员功能 +- 📚 **图书管理**: 添加、编辑、删除图书,支持批量导入 +- 👥 **学生管理**: 学生账户管理,权限控制 +- 📊 **借阅管理**: 处理借阅、归还、续借操作 +- 💰 **罚款管理**: 自动计算逾期罚款,处理缴费 +- 📈 **数据统计**: 实时统计和图表展示 +- ⚙️ **系统设置**: 灵活的系统参数配置 + +### 学生功能 +- 🔍 **图书检索**: 按书名、作者、ISBN等多维度搜索 +- 📖 **借阅管理**: 查看当前借阅、续借操作 +- 📅 **预约系统**: 预约已借出的热门图书 +- ⭐ **图书评价**: 对借阅图书进行评分和评论 +- 📋 **借阅历史**: 查看完整的借阅记录 +- 💳 **账户状态**: 实时查看借阅额度和罚款信息 + +## 🛠️ 技术栈 + +- **前端**: Next.js 15, React 19, TypeScript +- **样式**: Tailwind CSS +- **数据库**: PostgreSQL +- **ORM**: Drizzle ORM +- **状态管理**: TanStack Query (React Query) +- **表单处理**: React Hook Form + Zod +- **包管理**: Bun +- **图标**: Lucide React + +## 📋 系统要求 + +- Node.js 18.18+ 或 20+ +- Bun 1.0+ +- PostgreSQL 12+ + +## 🚀 快速开始 + +### 1. 克隆项目 +```bash +git clone +cd library_cs_design +``` + +### 2. 安装依赖 +```bash +bun install +``` + +### 3. 配置环境变量 +复制 `.env.example` 为 `.env.local` 并配置: +```env +# 数据库配置 +DATABASE_URL="postgresql://feie9454@127.0.0.1:5432/library" +DB_HOST="127.0.0.1" +DB_PORT="5432" +DB_USER="feie9454" +DB_PASSWORD="" +DB_NAME="library" + +# NextAuth配置 +NEXTAUTH_URL="http://localhost:3000" +NEXTAUTH_SECRET="your-secret-key-here" +``` + +### 4. 设置数据库 +确保PostgreSQL服务运行,并创建名为 `library` 的数据库和模式: +```sql +CREATE DATABASE library; +\c library; +CREATE SCHEMA library; +``` + +运行SQL脚本导入数据库结构和示例数据(参考附件中的SQL文件)。 + +### 5. 启动开发服务器 ```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or bun dev ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +应用将在 http://localhost:3000 启动。 -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +## 📁 项目结构 -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +``` +src/ +├── app/ # Next.js App Router页面 +│ ├── admin/ # 管理员界面 +│ ├── student/ # 学生界面 +│ ├── books/ # 图书相关页面 +│ └── api/ # API路由 +├── components/ # 可复用组件 +├── lib/ # 工具库 +│ ├── db/ # 数据库配置和模式 +│ ├── types/ # TypeScript类型定义 +│ └── utils/ # 工具函数 +└── hooks/ # 自定义React Hooks +``` -## Learn More +## 🗄️ 数据库设计 -To learn more about Next.js, take a look at the following resources: +### 核心表结构 +- `books` - 图书信息表 +- `students` - 学生信息表 +- `admins` - 管理员信息表 +- `borrow_records` - 借阅记录表 +- `reservations` - 预约记录表 +- `fines` - 罚款记录表 +- `reviews` - 图书评价表 +- `system_settings` - 系统设置表 -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +### 主要特性 +- 支持图书多作者存储(数组类型) +- 自动触发器处理库存数量 +- 完善的索引设计优化查询性能 +- 支持软删除和状态管理 -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +## 🔗 API接口 -## Deploy on Vercel +### 图书相关 +- `GET /api/books` - 获取图书列表(支持分页和搜索) +- `POST /api/books` - 添加新图书 +- `GET /api/books/[id]` - 获取图书详情 +- `PUT /api/books/[id]` - 更新图书信息 +- `DELETE /api/books/[id]` - 删除图书(软删除) -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +### 借阅相关 +- `POST /api/borrow` - 借阅图书 +- `POST /api/borrow/return` - 归还图书 +- `POST /api/borrow/renew` - 续借图书 +- `GET /api/borrow` - 获取借阅记录 -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +### 学生相关 +- `GET /api/students` - 获取学生列表 +- `POST /api/students` - 添加新学生 + +## 🎨 界面截图 + +### 主页 +- 现代化设计的欢迎页面 +- 清晰的功能模块展示 +- 快速访问入口 + +### 管理员控制台 +- 实时数据统计面板 +- 快速操作按钮 +- 最近活动展示 + +### 学生门户 +- 个人借阅状态概览 +- 当前借阅和预约管理 +- 便捷的操作界面 + +## 🧪 开发说明 + +### 代码规范 +- 使用TypeScript严格模式 +- ESLint + Prettier代码格式化 +- 组件使用函数式风格 +- API使用RESTful设计 + +### 数据库操作 +- 使用Drizzle ORM进行类型安全的数据库操作 +- 支持事务处理确保数据一致性 +- 合理使用索引优化查询性能 + +### 状态管理 +- 使用React Query管理服务器状态 +- 自动缓存和后台更新 +- 乐观更新提升用户体验 + +## 🔒 安全特性 + +- 密码哈希存储(bcrypt) +- SQL注入防护(参数化查询) +- 输入验证和清理 +- 权限控制和访问限制 + +## 📝 待实现功能 + +- [ ] 用户认证和会话管理 +- [ ] 图书批量导入功能 +- [ ] 邮件通知系统 +- [ ] 移动端响应式优化 +- [ ] 数据导出功能 +- [ ] 系统日志记录 +- [ ] 高级统计报表 + +## 🤝 贡献指南 + +1. Fork项目 +2. 创建特性分支 (`git checkout -b feature/AmazingFeature`) +3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) +4. 推送到分支 (`git push origin feature/AmazingFeature`) +5. 开启Pull Request + +## 📄 许可证 + +本项目采用MIT许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。 + +## 👥 作者 + +- **开发者** - 智能图书馆系统 + +## 🙏 致谢 + +- Next.js团队提供的优秀框架 +- Tailwind CSS的美观样式系统 +- Drizzle ORM的类型安全数据库操作 +- 所有开源项目贡献者 diff --git a/bun.lock b/bun.lock index f25bff9..4a656ad 100644 --- a/bun.lock +++ b/bun.lock @@ -4,9 +4,25 @@ "": { "name": "library_cs_design", "dependencies": { + "@hookform/resolvers": "^5.1.1", + "@tanstack/react-query": "^5.80.10", + "@tanstack/react-query-devtools": "^5.80.10", + "@types/bcryptjs": "^3.0.0", + "@types/pg": "^8.15.4", + "bcryptjs": "^3.0.2", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "drizzle-kit": "^0.31.1", + "drizzle-orm": "^0.44.2", + "lucide-react": "^0.520.0", "next": "15.3.4", + "next-auth": "^4.24.11", + "pg": "^8.16.2", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hook-form": "^7.58.1", + "tailwind-merge": "^3.3.1", + "zod": "^3.25.67", }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -26,12 +42,70 @@ "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + "@babel/runtime": ["@babel/runtime@7.27.6", "", {}, "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q=="], + + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + "@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" } }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="], "@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="], "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="], + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], + + "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.5", "", { "os": "android", "cpu": "arm64" }, "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.5", "", { "os": "android", "cpu": "x64" }, "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.5", "", { "os": "linux", "cpu": "arm" }, "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.5", "", { "os": "linux", "cpu": "x64" }, "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.5", "", { "os": "none", "cpu": "arm64" }, "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.5", "", { "os": "none", "cpu": "x64" }, "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="], + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], @@ -50,6 +124,8 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.2", "", { "dependencies": { "@eslint/core": "^0.15.0", "levn": "^0.4.1" } }, "sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg=="], + "@hookform/resolvers": ["@hookform/resolvers@5.1.1", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-J/NVING3LMAEvexJkyTLjruSm7aOFx7QX21pzkiJfMoNG0wl5aFEjLTl7ay7IQb9EWY6AkrBy7tHL2Alijpdcg=="], + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], @@ -142,10 +218,14 @@ "@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="], + "@panva/hkdf": ["@panva/hkdf@1.2.1", "", {}, "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw=="], + "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], "@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.11.0", "", {}, "sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ=="], + "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], + "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], @@ -180,8 +260,18 @@ "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.10", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.10", "@tailwindcss/oxide": "4.1.10", "postcss": "^8.4.41", "tailwindcss": "4.1.10" } }, "sha512-B+7r7ABZbkXJwpvt2VMnS6ujcDoR2OOcFaqrLIo1xbcdxje4Vf+VgJdBzNNbrAjBj/rLZ66/tlQ1knIGNLKOBQ=="], + "@tanstack/query-core": ["@tanstack/query-core@5.80.10", "", {}, "sha512-mUNQOtzxkjL6jLbyChZoSBP6A5gQDVRUiPvW+/zw/9ftOAz+H754zCj3D8PwnzPKyHzGkQ9JbH48ukhym9LK1Q=="], + + "@tanstack/query-devtools": ["@tanstack/query-devtools@5.80.0", "", {}, "sha512-D6gH4asyjaoXrCOt5vG5Og/YSj0D/TxwNQgtLJIgWbhbWCC/emu2E92EFoVHh4ppVWg1qT2gKHvKyQBEFZhCuA=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.80.10", "", { "dependencies": { "@tanstack/query-core": "5.80.10" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-6zM098J8sLy9oU60XAdzUlAH4wVzoMVsWUWiiE/Iz4fd67PplxeyL4sw/MPcVJJVhbwGGXCsHn9GrQt2mlAzig=="], + + "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.80.10", "", { "dependencies": { "@tanstack/query-devtools": "5.80.0" }, "peerDependencies": { "@tanstack/react-query": "^5.80.10", "react": "^18 || ^19" } }, "sha512-6JL63fSc7kxyGOLV2w466SxhMn/m7LZk/ximQciy6OpVt+n2A8Mq3S0QwhIzfm4WEwLK/F3OELfzRToQburnYA=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], + "@types/bcryptjs": ["@types/bcryptjs@3.0.0", "", { "dependencies": { "bcryptjs": "*" } }, "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], @@ -190,6 +280,8 @@ "@types/node": ["@types/node@20.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA=="], + "@types/pg": ["@types/pg@8.15.4", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg=="], + "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], "@types/react-dom": ["@types/react-dom@19.1.6", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw=="], @@ -292,10 +384,14 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "bcryptjs": ["bcryptjs@3.0.2", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog=="], + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="], "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], @@ -314,6 +410,8 @@ "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -324,6 +422,8 @@ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], @@ -336,6 +436,8 @@ "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], @@ -348,6 +450,10 @@ "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + "drizzle-kit": ["drizzle-kit@0.31.1", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.2", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-PUjYKWtzOzPtdtQlTHQG3qfv4Y0XT8+Eas6UbxCmxTj7qgMf+39dDujf1BP1I+qqZtw9uzwTh8jYtkMuCq+B0Q=="], + + "drizzle-orm": ["drizzle-orm@0.44.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-zGAqBzWWkVSFjZpwPOrmCrgO++1kZ5H/rZ4qTGeGOe18iXGVJWf3WPfHOVwFIbmi8kHjfJstC6rJomzGx8g/dQ=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], @@ -370,6 +476,10 @@ "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + "esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="], + + "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "eslint": ["eslint@9.29.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.1", "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.29.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ=="], @@ -538,6 +648,8 @@ "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], + "jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], @@ -588,6 +700,10 @@ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "lucide-react": ["lucide-react@0.520.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Mvo4mGi1X0iygX3x1Zy5UwjeiBvJOTZe7/c9Z0vLo67E9yOtIIuYusfE2HDwvR3FM5osU0tqTc2tn1aKtfUV8w=="], + "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -616,8 +732,14 @@ "next": ["next@15.3.4", "", { "dependencies": { "@next/env": "15.3.4", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.4", "@next/swc-darwin-x64": "15.3.4", "@next/swc-linux-arm64-gnu": "15.3.4", "@next/swc-linux-arm64-musl": "15.3.4", "@next/swc-linux-x64-gnu": "15.3.4", "@next/swc-linux-x64-musl": "15.3.4", "@next/swc-win32-arm64-msvc": "15.3.4", "@next/swc-win32-x64-msvc": "15.3.4", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-mHKd50C+mCjam/gcnwqL1T1vPx/XQNFlXqFIVdgQdVAFY9iIQtY0IfaVflEYzKiqjeA7B0cYYMaCrmAYFjs4rA=="], + "next-auth": ["next-auth@4.24.11", "", { "dependencies": { "@babel/runtime": "^7.20.13", "@panva/hkdf": "^1.0.2", "cookie": "^0.7.0", "jose": "^4.15.5", "oauth": "^0.9.15", "openid-client": "^5.4.0", "preact": "^10.6.3", "preact-render-to-string": "^5.1.19", "uuid": "^8.3.2" }, "peerDependencies": { "@auth/core": "0.34.2", "next": "^12.2.5 || ^13 || ^14 || ^15", "nodemailer": "^6.6.5", "react": "^17.0.2 || ^18 || ^19", "react-dom": "^17.0.2 || ^18 || ^19" }, "optionalPeers": ["@auth/core", "nodemailer"] }, "sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw=="], + + "oauth": ["oauth@0.9.15", "", {}, "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + "object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="], + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], @@ -632,6 +754,10 @@ "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + "oidc-token-hash": ["oidc-token-hash@5.1.0", "", {}, "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA=="], + + "openid-client": ["openid-client@5.7.1", "", { "dependencies": { "jose": "^4.15.9", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], @@ -648,6 +774,22 @@ "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "pg": ["pg@8.16.2", "", { "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", "pg-protocol": "^1.10.2", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.6" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-OtLWF0mKLmpxelOt9BqVq83QV6bTfsS0XLegIeAKqKjurRnRKie1Dc1iL89MugmSLhftxw6NNCyZhm1yQFLMEQ=="], + + "pg-cloudflare": ["pg-cloudflare@1.2.6", "", {}, "sha512-uxmJAnmIgmYgnSFzgOf2cqGQBzwnRYcrEgXuFjJNEkpedEIPBSEzxY7ph4uA9k1mI+l/GR0HjPNS6FKNZe8SBQ=="], + + "pg-connection-string": ["pg-connection-string@2.9.1", "", {}, "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="], + + "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], + + "pg-pool": ["pg-pool@3.10.1", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg=="], + + "pg-protocol": ["pg-protocol@1.10.2", "", {}, "sha512-Ci7jy8PbaWxfsck2dwZdERcDG2A0MG8JoQILs+uZNjABFuBuItAZCWUNz8sXRDMoui24rJw7WlXqgpMdBSN/vQ=="], + + "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], + + "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], @@ -656,8 +798,22 @@ "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], + + "postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="], + + "postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], + + "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], + + "preact": ["preact@10.26.9", "", {}, "sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA=="], + + "preact-render-to-string": ["preact-render-to-string@5.2.6", "", { "dependencies": { "pretty-format": "^3.8.0" }, "peerDependencies": { "preact": ">=10" } }, "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw=="], + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + "pretty-format": ["pretty-format@3.8.0", "", {}, "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="], + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], @@ -668,6 +824,8 @@ "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="], + "react-hook-form": ["react-hook-form@7.58.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-Lml/KZYEEFfPhUVgE0RdCVpnC4yhW+PndRhbiTtdvSlQTL8IfVR+iQkBjLIvmmc6+GGoVeM11z37ktKFPAb0FA=="], + "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], @@ -716,8 +874,14 @@ "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + "stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="], "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], @@ -746,6 +910,8 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="], + "tailwindcss": ["tailwindcss@4.1.10", "", {}, "sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA=="], "tapable": ["tapable@2.2.2", "", {}, "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg=="], @@ -782,6 +948,8 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], @@ -794,10 +962,16 @@ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "zod": ["zod@3.25.67", "", {}, "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw=="], + + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.15.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw=="], @@ -836,12 +1010,58 @@ "is-bun-module/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], "sharp/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "@typescript-eslint/typescript-estree/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], diff --git a/create_library.sql b/create_library.sql new file mode 100644 index 0000000..74b70f3 --- /dev/null +++ b/create_library.sql @@ -0,0 +1,239 @@ +SET search_path TO library, public; + +-- 清理可能存在的旧数据(可选,测试时方便重跑) +-- 请注意,CASCADE 会删除关联数据,请谨慎在生产环境使用 +-- DELETE FROM fines; +-- DELETE FROM reviews; +-- DELETE FROM reservations; +-- DELETE FROM borrow_records; +-- DELETE FROM admins; +-- DELETE FROM students; +-- DELETE FROM books; +-- DELETE FROM system_settings WHERE setting_key NOT IN ('fine_per_day', 'freeze_threshold', 'max_borrow_default'); -- 保留核心设置 + +-- 0. 系统参数 (你已插入,这里可以补充更多,如果需要) +INSERT INTO system_settings(setting_key, setting_value) VALUES +('max_renew_times', '2'), -- 最大续借次数 +('borrow_duration_days', '30'), -- 默认借阅时长(天) +('reservation_expiry_days', '3'), -- 预约保留天数 +('admin_default_password_hash', 'xxxx'); -- 示例:管理员默认密码哈希 +-- ON CONFLICT (setting_key) DO UPDATE SET setting_value = EXCLUDED.setting_value; -- 如果要允许更新 + +-- 1. 管理员信息 (Admins) +INSERT INTO admins (emp_no, name, position, phone, privilege_lv) VALUES +('A001', '张管理', '馆长', '13800138000', 0), -- 最高权限 +('A002', '李协助', '图书管理员', '13900139000', 1), +('A003', '王登记', '流通部职员', '13700137000', 2); + +-- 2. 图书信息 (Books) +-- 注意: available_copies 初始设为 total_copies,后续借阅会通过触发器减少 +INSERT INTO books (isbn, title, authors, publisher, publish_date, price, classification_no, location, total_copies, available_copies, status, description, cover_url) VALUES +('9787111624927', '深入理解计算机系统', ARRAY['Randal E. Bryant', 'David R. O''Hallaron'], '机械工业出版社', '2019-05-01', 139.00, 'TP301', 'A区2架3层', 10, 10, 'normal', '计算机系统的经典之作。', 'http://example.com/cover1.jpg'), +('9787030598007', '数学之美', ARRAY['吴军'], '科学出版社', '2020-01-01', 68.00, 'O1', 'B区1架1层', 15, 15, 'normal', '通俗易懂的数学科普读物。', 'http://example.com/cover2.jpg'), +('9787544270878', '百年孤独', ARRAY['加西亚·马尔克斯'], '南海出版公司', '2011-06-01', 39.50, 'I775.45', 'C区5架2层', 8, 8, 'normal', '魔幻现实主义文学的代表作。', 'http://example.com/cover3.jpg'), +('9787115478850', 'Python编程:从入门到实践', ARRAY['Eric Matthes'], '人民邮电出版社', '2018-03-01', 89.00, 'TP312PY', 'A区3架1层', 12, 12, 'normal', 'Python入门畅销书。', 'http://example.com/cover4.jpg'), +('9787508647009', '人类简史:从动物到上帝', ARRAY['尤瓦尔·赫拉利'], '中信出版社', '2014-11-01', 68.00, 'K02', 'D区1架1层', 20, 20, 'normal', '一部宏大的人类发展史。', 'http://example.com/cover5.jpg'), +('9787532767406', '追风筝的人', ARRAY['卡勒德·胡赛尼'], '上海人民出版社', '2006-05-01', 29.00, 'I712.45', 'C区4架3层', 7, 7, 'normal', '关于爱、友谊、背叛和救赎的故事。', 'http://example.com/cover6.jpg'), +('9787121362308', 'Java核心技术 卷I', ARRAY['Cay S. Horstmann'], '电子工业出版社', '2019-06-01', 128.00, 'TP312JA', 'A区3架2层', 5, 0, 'normal', 'Java经典教材,目前全部被借出,用于测试预约。', 'http://example.com/cover7.jpg'), -- 特意设置 available_copies 为 0 +('9787208159659', '三体全集', ARRAY['刘慈欣'], '重庆出版社', '2019-01-01', 168.00, 'I247.5', 'C区6架1层', 3, 3, 'damaged', '科幻巨作,其中一本有损坏。', 'http://example.com/cover8.jpg'), +('9787559620187', '明朝那些事儿 (套装全7册)', ARRAY['当年明月'], '北京联合出版公司', '2017-08-01', 258.00, 'K248', 'D区2架1层', 6, 6, 'normal', '通俗易懂的明史。', 'http://example.com/cover9.jpg'), +('9787111600815', '算法导论 (原书第3版)', ARRAY['Thomas H. Cormen'], '机械工业出版社', '2012-12-01', 128.00, 'TP301.6', 'A区2架4层', 9, 9, 'normal', '算法领域的圣经。', 'http://example.com/cover10.jpg'), +('9780132350884', 'Clean Code', ARRAY['Robert C. Martin'], 'Prentice Hall', '2008-08-01', 50.00, 'TP311.1', 'A区1架5层', 4, 4, 'removed', '一本已下架的书,测试状态。', 'http://example.com/cover11.jpg'); + + +-- 3. 学生信息 (Students) +-- max_borrow 使用默认值(通过 get_setting_int('max_borrow_default') 获取,即10) +-- current_borrow 初始为0,会由触发器更新 +INSERT INTO students (stu_no, name, gender, department, major, grade, class, phone, email, account_status) VALUES +('S2023001', '张三', 'M', '计算机学院', '软件工程', '2023', '01班', '13512345671', 'zhangsan@example.com', 'active'), +('S2023002', '李四', 'F', '计算机学院', '人工智能', '2023', '02班', '13512345672', 'lisi@example.com', 'active'), +('S2022003', '王五', 'M', '外国语学院', '英语', '2022', '01班', '13512345673', 'wangwu@example.com', 'active'), +('S2021004', '赵六', 'F', '经济管理学院', '工商管理', '2021', '03班', '13512345674', 'zhaoliu@example.com', 'active'), +('S2023005', '孙七', 'M', '计算机学院', '软件工程', '2023', '01班', '13512345675', 'sunqi@example.com', 'reported'), -- 测试挂失状态 +('S2022006', '周八', 'F', '人文学院', '历史学', '2022', '02班', '13512345676', 'zhouba@example.com', 'active'), +('S2021007', '吴九', 'M', '理学院', '数学与应用数学', '2021', '01班', '13512345677', 'wujiu@example.com', 'active'), +('S2023008', '郑十', 'F', '计算机学院', '网络工程', '2023', '03班', '13512345678', 'zhengshi@example.com', 'active'); + +-- 修改一个学生的 max_borrow,测试非默认值 +UPDATE students SET max_borrow = 15 WHERE stu_no = 'S2023001'; +UPDATE students SET max_borrow = 5 WHERE stu_no = 'S2021004'; -- 用于测试借阅上限 + +-- 4. 借阅记录 (Borrow Records) +-- book_id 和 student_id 需要引用已存在的 ID +-- due_date = borrow_date + (SELECT setting_value::int FROM system_settings WHERE setting_key = 'borrow_duration_days') +-- fine_amount 初始为0, 逾期归还将由触发器在 fines 表中记录罚款 +-- 触发器 trg_sync_book_student 会在每次插入/更新/删除后执行 + +-- 获取 book_id 和 student_id 的示例 (实际使用时,你可能需要根据书名/学号查询得到) +-- SELECT book_id FROM books WHERE isbn = '9787111624927'; -- 1 +-- SELECT student_id FROM students WHERE stu_no = 'S2023001'; -- 1 + +-- 借阅场景 1: 正常借出,未到期 +INSERT INTO borrow_records (book_id, student_id, borrow_date, due_date, status) VALUES +(1, 1, current_date - interval '10 days', current_date - interval '10 days' + interval '30 days', 'borrowed'), -- 张三借《深入理解计算机系统》 +(2, 2, current_date - interval '5 days', current_date - interval '5 days' + interval '30 days', 'borrowed'); -- 李四借《数学之美》 + +-- 借阅场景 2: 正常借出,今天到期 +INSERT INTO borrow_records (book_id, student_id, borrow_date, due_date, status) VALUES +(3, 1, current_date - interval '30 days', current_date, 'borrowed'); -- 张三借《百年孤独》,今天到期 + +-- 借阅场景 3: 已逾期,未归还 +INSERT INTO borrow_records (book_id, student_id, borrow_date, due_date, status) VALUES +(4, 3, current_date - interval '40 days', current_date - interval '10 days', 'overdue'); -- 王五借《Python编程》,已逾期10天 + +-- 借阅场景 4: 正常归还 (触发器 trg_calc_fine 不会产生罚款) +INSERT INTO borrow_records (book_id, student_id, borrow_date, due_date, return_date, status) VALUES +(5, 4, current_date - interval '20 days', current_date + interval '10 days', current_date - interval '2 days', 'returned'); -- 赵六借《人类简史》,已按时归还 + +-- 借阅场景 5: 逾期归还 (为了测试 trg_calc_fine,先插入为逾期,再UPDATE为归还) +-- 步骤A: 先插入一条记录,状态为 'overdue' (或者 'borrowed' 但实际已过 due_date) +INSERT INTO borrow_records (book_id, student_id, borrow_date, due_date, status) +VALUES (6, 5, current_date - interval '35 days', current_date - interval '5 days', 'overdue'); -- 孙七借《追风筝的人》 +-- 记录下这条 borrow_id (假设是 6,根据实际情况调整) +-- 步骤B: 更新这条记录为 'returned',这将触发 trg_calc_fine +-- UPDATE borrow_records +-- SET status = 'returned', return_date = current_date +-- WHERE borrow_id = (SELECT borrow_id FROM borrow_records WHERE student_id=5 AND book_id=6 AND status='overdue'); +-- (该update放到下面演示触发器部分) + +-- 借阅场景 6: 书籍遗失 +INSERT INTO borrow_records (book_id, student_id, borrow_date, due_date, status) VALUES +(9, 6, current_date - interval '15 days', current_date + interval '15 days', 'lost'); -- 周八借《明朝那些事儿》,遗失 + + +-- 5. 图书预约 (Reservations) +-- 只能预约 available_copies = 0 的书 +-- (book_id=7, 'Java核心技术 卷I', available_copies 初始为0) +INSERT INTO reservations (book_id, student_id, reserve_date, status) VALUES +(7, 1, current_date - interval '2 days', 'waiting'), -- 张三预约《Java核心技术》 +(7, 2, current_date - interval '1 day', 'waiting'); -- 李四预约《Java核心技术》 + +-- 假设一本被预约的书有归还了,管理员将其状态改为可取 +-- UPDATE reservations SET status = 'available' WHERE book_id = 7 AND student_id = 1; + +-- 6. 图书评价 (Reviews) +-- 学生通常评价已借阅过的书 +INSERT INTO reviews (book_id, student_id, rating, content, review_time) VALUES +(5, 4, 5, '《人类简史》这本书太棒了,视角独特,值得一读!', now() - interval '1 day'), -- 赵六评价《人类简史》 +(1, 1, 4, '《深入理解计算机系统》有点难,但很有收获。', now()), -- 张三评价 +(2, 2, 5, '《数学之美》让我对数学有了新的认识。', now()); -- 李四评价 + +-- 尝试插入重复评价(应失败,因为有UNIQUE约束) +-- INSERT INTO reviews (book_id, student_id, rating, content, review_time) VALUES +-- (5, 4, 3, '第二次评价,内容一般。', now()); + +-- 7. 罚款记录 (Fines) +-- 部分罚款会由 trg_calc_fine 自动生成 (当逾期借阅记录更新为 'returned' 时) +-- 手动添加一些罚款记录: + +-- 罚款场景1: 图书损坏 (假设管理员A002处理) +INSERT INTO fines (student_id, amount, reason, status, issue_date, admin_id) VALUES +( (SELECT student_id FROM students WHERE stu_no = 'S2022003'), -- 王五 + 20.00, + '损坏图书《Python编程》(ISBN:9787115478850)', + 'unpaid', + current_date - interval '1 day', + (SELECT admin_id FROM admins WHERE emp_no = 'A002') +); + +-- 罚款场景2: 图书遗失 (假设管理员A002处理) +-- 对应 borrow_records 中周八遗失的《明朝那些事儿》 +INSERT INTO fines (student_id, amount, reason, status, issue_date, admin_id) VALUES +( (SELECT student_id FROM students WHERE stu_no = 'S2022006'), -- 周八 + (SELECT price FROM books WHERE isbn = '9787559620187'), -- 按书价赔偿 + '遗失图书《明朝那些事儿》(ISBN:9787559620187)', + 'unpaid', + current_date, + (SELECT admin_id FROM admins WHERE emp_no = 'A002') +); + +-- 罚款场景3: 之前的逾期罚款,已缴纳 +INSERT INTO fines (student_id, amount, reason, status, issue_date, admin_id) VALUES +( (SELECT student_id FROM students WHERE stu_no = 'S2021007'), -- 吴九 + 5.50, + '图书逾期11天 (旧记录)', + 'paid', + current_date - interval '60 days', + (SELECT admin_id FROM admins WHERE emp_no = 'A003') +); + + +---------------------------------------------------- +-- 测试触发器 trg_calc_fine 和 trg_freeze_account +---------------------------------------------------- +DO $$ +DECLARE + v_student_id_sunqi bigint; + v_borrow_id_sunqi bigint; + v_fine_per_day numeric; + v_days_overdue int; + v_expected_fine numeric; +BEGIN + -- 获取孙七的 student_id + SELECT student_id INTO v_student_id_sunqi FROM students WHERE stu_no = 'S2023005'; + -- 获取孙七之前插入的逾期借阅记录 (book_id=6, 追风筝的人) + SELECT borrow_id INTO v_borrow_id_sunqi + FROM borrow_records + WHERE student_id = v_student_id_sunqi AND book_id = (SELECT book_id FROM books WHERE isbn='9787532767406') AND status = 'overdue'; + + RAISE NOTICE '孙七(ID:%)的借阅记录ID: % 将被更新为已归还。', v_student_id_sunqi, v_borrow_id_sunqi; + + -- 模拟孙七归还之前逾期的书 (book_id=6, 《追风筝的人》) + -- 这将触发 trg_calc_fine + UPDATE borrow_records + SET status = 'returned', return_date = current_date + WHERE borrow_id = v_borrow_id_sunqi; + + RAISE NOTICE '已更新孙七的借阅记录。现在检查 fines 表是否自动产生罚款记录...'; + -- trg_calc_fine 会根据 borrow_records 中的 due_date 和 return_date 计算罚款并插入 fines 表 + -- 假设罚款汇率是 0.5/天,逾期5天,罚款应为 2.50 + -- (current_date - (current_date - interval '5 days')) * 0.50 + SELECT setting_value::numeric INTO v_fine_per_day FROM system_settings WHERE setting_key = 'fine_per_day'; + SELECT (current_date - (SELECT due_date FROM borrow_records WHERE borrow_id = v_borrow_id_sunqi)) INTO v_days_overdue; + v_expected_fine := v_days_overdue * v_fine_per_day; + RAISE NOTICE '预期罚款金额: % * % = %', v_days_overdue, v_fine_per_day, v_expected_fine; + + -- 检查孙七的账户状态是否因为罚款超过阈值 (默认20) 而被冻结 + -- trg_freeze_account 会在 fines 表 INSERT/UPDATE/DELETE 后触发 + RAISE NOTICE '检查孙七账户是否因罚款自动冻结...'; + -- 如果孙七的罚款 (例如 2.50) 没有超过 freeze_threshold (默认20.00),账户不会冻结。 + -- 我们再为孙七手动添加一笔大额罚款,使其总欠款超过阈值 + IF (SELECT sum(amount) FROM fines WHERE student_id = v_student_id_sunqi AND status = 'unpaid') < (SELECT setting_value::numeric FROM system_settings WHERE setting_key = 'freeze_threshold') THEN + RAISE NOTICE '孙七当前未付罚款未达冻结阈值,为其添加一笔大额罚款...'; + INSERT INTO fines (student_id, amount, reason, status, issue_date, admin_id) VALUES + (v_student_id_sunqi, 25.00, '严重损坏图书赔偿', 'unpaid', current_date, (SELECT admin_id FROM admins WHERE emp_no='A001')); + RAISE NOTICE '已为孙七添加大额罚款,再次检查账户状态。'; + END IF; + +END $$; + +-- 查询验证触发器效果 +RAISE NOTICE '---------- 查询验证 ----------'; + +RAISE NOTICE '1. 书籍可借数量和学生当前借阅量 (应由 trg_sync_book_student 更新):'; +SELECT book_id, title, total_copies, available_copies FROM books WHERE book_id <= 7; +SELECT student_id, name, max_borrow, current_borrow, account_status FROM students WHERE student_id <= 5; + +RAISE NOTICE '2. 孙七 (S2023005) 的罚款记录 (应有 trg_calc_fine 生成的逾期罚款 和 手动添加的罚款):'; +SELECT f.*, s.name as student_name +FROM fines f JOIN students s ON f.student_id = s.student_id +WHERE s.stu_no = 'S2023005'; + +RAISE NOTICE '3. 孙七 (S2023005) 的账户状态 (应由 trg_freeze_account 根据总欠款更新):'; +SELECT stu_no, name, account_status, current_borrow +FROM students +WHERE stu_no = 'S2023005'; + +RAISE NOTICE '4. 查看热门图书视图:'; +SELECT * FROM v_hot_books; + +RAISE NOTICE '5. 查看院系借阅统计视图:'; +SELECT * FROM v_dept_borrow_stats; + +RAISE NOTICE '6. 查看逾期详情视图:'; +SELECT od.*, b.title, s.name as student_name +FROM v_overdue_details od +JOIN books b ON od.book_id = b.book_id +JOIN students s ON od.student_id = s.student_id; + +RAISE NOTICE '测试数据插入完毕。'; \ No newline at end of file diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..06d9b24 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + dialect: 'postgresql', + schema: './src/lib/db/schema.ts', + out: './drizzle', + dbCredentials: { + host: process.env.DB_HOST!, + port: parseInt(process.env.DB_PORT!), + user: process.env.DB_USER!, + password: process.env.DB_PASSWORD!, + database: process.env.DB_NAME!, + }, +}); diff --git a/example_data.sql b/example_data.sql new file mode 100644 index 0000000..27a894a --- /dev/null +++ b/example_data.sql @@ -0,0 +1,239 @@ +SET search_path TO library, public; + +-- 清理可能存在的旧数据(可选,测试时方便重跑) +-- 请注意,CASCADE 会删除关联数据,请谨慎在生产环境使用 +DELETE FROM fines; +DELETE FROM reviews; +DELETE FROM reservations; +DELETE FROM borrow_records; +DELETE FROM admins; +DELETE FROM students; +DELETE FROM books; +DELETE FROM system_settings WHERE setting_key NOT IN ('fine_per_day', 'freeze_threshold', 'max_borrow_default'); -- 保留核心设置 + +-- 0. 系统参数 (你已插入,这里可以补充更多,如果需要) +INSERT INTO system_settings(setting_key, setting_value) VALUES +('max_renew_times', '2'), -- 最大续借次数 +('borrow_duration_days', '30'), -- 默认借阅时长(天) +('reservation_expiry_days', '3'), -- 预约保留天数 +('admin_default_password_hash', 'xxxx'); -- 示例:管理员默认密码哈希 +-- ON CONFLICT (setting_key) DO UPDATE SET setting_value = EXCLUDED.setting_value; -- 如果要允许更新 + +-- 1. 管理员信息 (Admins) +INSERT INTO admins (emp_no, name, position, phone, privilege_lv) VALUES +('A001', '张管理', '馆长', '13800138000', 0), -- 最高权限 +('A002', '李协助', '图书管理员', '13900139000', 1), +('A003', '王登记', '流通部职员', '13700137000', 2); + +-- 2. 图书信息 (Books) +-- 注意: available_copies 初始设为 total_copies,后续借阅会通过触发器减少 +INSERT INTO books (isbn, title, authors, publisher, publish_date, price, classification_no, location, total_copies, available_copies, status, description, cover_url) VALUES +('9787111624927', '深入理解计算机系统', ARRAY['Randal E. Bryant', 'David R. O''Hallaron'], '机械工业出版社', '2019-05-01', 139.00, 'TP301', 'A区2架3层', 10, 10, 'normal', '计算机系统的经典之作。', 'http://example.com/cover1.jpg'), +('9787030598007', '数学之美', ARRAY['吴军'], '科学出版社', '2020-01-01', 68.00, 'O1', 'B区1架1层', 15, 15, 'normal', '通俗易懂的数学科普读物。', 'http://example.com/cover2.jpg'), +('9787544270878', '百年孤独', ARRAY['加西亚·马尔克斯'], '南海出版公司', '2011-06-01', 39.50, 'I775.45', 'C区5架2层', 8, 8, 'normal', '魔幻现实主义文学的代表作。', 'http://example.com/cover3.jpg'), +('9787115478850', 'Python编程:从入门到实践', ARRAY['Eric Matthes'], '人民邮电出版社', '2018-03-01', 89.00, 'TP312PY', 'A区3架1层', 12, 12, 'normal', 'Python入门畅销书。', 'http://example.com/cover4.jpg'), +('9787508647009', '人类简史:从动物到上帝', ARRAY['尤瓦尔·赫拉利'], '中信出版社', '2014-11-01', 68.00, 'K02', 'D区1架1层', 20, 20, 'normal', '一部宏大的人类发展史。', 'http://example.com/cover5.jpg'), +('9787532767406', '追风筝的人', ARRAY['卡勒德·胡赛尼'], '上海人民出版社', '2006-05-01', 29.00, 'I712.45', 'C区4架3层', 7, 7, 'normal', '关于爱、友谊、背叛和救赎的故事。', 'http://example.com/cover6.jpg'), +('9787121362308', 'Java核心技术 卷I', ARRAY['Cay S. Horstmann'], '电子工业出版社', '2019-06-01', 128.00, 'TP312JA', 'A区3架2层', 5, 0, 'normal', 'Java经典教材,目前全部被借出,用于测试预约。', 'http://example.com/cover7.jpg'), -- 特意设置 available_copies 为 0 +('9787208159659', '三体全集', ARRAY['刘慈欣'], '重庆出版社', '2019-01-01', 168.00, 'I247.5', 'C区6架1层', 3, 3, 'damaged', '科幻巨作,其中一本有损坏。', 'http://example.com/cover8.jpg'), +('9787559620187', '明朝那些事儿 (套装全7册)', ARRAY['当年明月'], '北京联合出版公司', '2017-08-01', 258.00, 'K248', 'D区2架1层', 6, 6, 'normal', '通俗易懂的明史。', 'http://example.com/cover9.jpg'), +('9787111600815', '算法导论 (原书第3版)', ARRAY['Thomas H. Cormen'], '机械工业出版社', '2012-12-01', 128.00, 'TP301.6', 'A区2架4层', 9, 9, 'normal', '算法领域的圣经。', 'http://example.com/cover10.jpg'), +('9780132350884', 'Clean Code', ARRAY['Robert C. Martin'], 'Prentice Hall', '2008-08-01', 50.00, 'TP311.1', 'A区1架5层', 4, 4, 'removed', '一本已下架的书,测试状态。', 'http://example.com/cover11.jpg'); + + +-- 3. 学生信息 (Students) +-- max_borrow 使用默认值(通过 get_setting_int('max_borrow_default') 获取,即10) +-- current_borrow 初始为0,会由触发器更新 +INSERT INTO students (stu_no, name, gender, department, major, grade, class, phone, email, account_status) VALUES +('S2023001', '张三', 'M', '计算机学院', '软件工程', '2023', '01班', '13512345671', 'zhangsan@example.com', 'active'), +('S2023002', '李四', 'F', '计算机学院', '人工智能', '2023', '02班', '13512345672', 'lisi@example.com', 'active'), +('S2022003', '王五', 'M', '外国语学院', '英语', '2022', '01班', '13512345673', 'wangwu@example.com', 'active'), +('S2021004', '赵六', 'F', '经济管理学院', '工商管理', '2021', '03班', '13512345674', 'zhaoliu@example.com', 'active'), +('S2023005', '孙七', 'M', '计算机学院', '软件工程', '2023', '01班', '13512345675', 'sunqi@example.com', 'reported'), -- 测试挂失状态 +('S2022006', '周八', 'F', '人文学院', '历史学', '2022', '02班', '13512345676', 'zhouba@example.com', 'active'), +('S2021007', '吴九', 'M', '理学院', '数学与应用数学', '2021', '01班', '13512345677', 'wujiu@example.com', 'active'), +('S2023008', '郑十', 'F', '计算机学院', '网络工程', '2023', '03班', '13512345678', 'zhengshi@example.com', 'active'); + +-- 修改一个学生的 max_borrow,测试非默认值 +UPDATE students SET max_borrow = 15 WHERE stu_no = 'S2023001'; +UPDATE students SET max_borrow = 5 WHERE stu_no = 'S2021004'; -- 用于测试借阅上限 + +-- 4. 借阅记录 (Borrow Records) +-- book_id 和 student_id 需要引用已存在的 ID +-- due_date = borrow_date + (SELECT setting_value::int FROM system_settings WHERE setting_key = 'borrow_duration_days') +-- fine_amount 初始为0, 逾期归还将由触发器在 fines 表中记录罚款 +-- 触发器 trg_sync_book_student 会在每次插入/更新/删除后执行 + +-- 获取 book_id 和 student_id 的示例 (实际使用时,你可能需要根据书名/学号查询得到) +-- SELECT book_id FROM books WHERE isbn = '9787111624927'; -- 1 +-- SELECT student_id FROM students WHERE stu_no = 'S2023001'; -- 1 + +-- 借阅场景 1: 正常借出,未到期 +INSERT INTO borrow_records (book_id, student_id, borrow_date, due_date, status) VALUES +(1, 1, current_date - interval '10 days', current_date - interval '10 days' + interval '30 days', 'borrowed'), -- 张三借《深入理解计算机系统》 +(2, 2, current_date - interval '5 days', current_date - interval '5 days' + interval '30 days', 'borrowed'); -- 李四借《数学之美》 + +-- 借阅场景 2: 正常借出,今天到期 +INSERT INTO borrow_records (book_id, student_id, borrow_date, due_date, status) VALUES +(3, 1, current_date - interval '30 days', current_date, 'borrowed'); -- 张三借《百年孤独》,今天到期 + +-- 借阅场景 3: 已逾期,未归还 +INSERT INTO borrow_records (book_id, student_id, borrow_date, due_date, status) VALUES +(4, 3, current_date - interval '40 days', current_date - interval '10 days', 'overdue'); -- 王五借《Python编程》,已逾期10天 + +-- 借阅场景 4: 正常归还 (触发器 trg_calc_fine 不会产生罚款) +INSERT INTO borrow_records (book_id, student_id, borrow_date, due_date, return_date, status) VALUES +(5, 4, current_date - interval '20 days', current_date + interval '10 days', current_date - interval '2 days', 'returned'); -- 赵六借《人类简史》,已按时归还 + +-- 借阅场景 5: 逾期归还 (为了测试 trg_calc_fine,先插入为逾期,再UPDATE为归还) +-- 步骤A: 先插入一条记录,状态为 'overdue' (或者 'borrowed' 但实际已过 due_date) +INSERT INTO borrow_records (book_id, student_id, borrow_date, due_date, status) +VALUES (6, 5, current_date - interval '35 days', current_date - interval '5 days', 'overdue'); -- 孙七借《追风筝的人》 +-- 记录下这条 borrow_id (假设是 6,根据实际情况调整) +-- 步骤B: 更新这条记录为 'returned',这将触发 trg_calc_fine +-- UPDATE borrow_records +-- SET status = 'returned', return_date = current_date +-- WHERE borrow_id = (SELECT borrow_id FROM borrow_records WHERE student_id=5 AND book_id=6 AND status='overdue'); +-- (该update放到下面演示触发器部分) + +-- 借阅场景 6: 书籍遗失 +INSERT INTO borrow_records (book_id, student_id, borrow_date, due_date, status) VALUES +(9, 6, current_date - interval '15 days', current_date + interval '15 days', 'lost'); -- 周八借《明朝那些事儿》,遗失 + + +-- 5. 图书预约 (Reservations) +-- 只能预约 available_copies = 0 的书 +-- (book_id=7, 'Java核心技术 卷I', available_copies 初始为0) +INSERT INTO reservations (book_id, student_id, reserve_date, status) VALUES +(7, 1, current_date - interval '2 days', 'waiting'), -- 张三预约《Java核心技术》 +(7, 2, current_date - interval '1 day', 'waiting'); -- 李四预约《Java核心技术》 + +-- 假设一本被预约的书有归还了,管理员将其状态改为可取 +-- UPDATE reservations SET status = 'available' WHERE book_id = 7 AND student_id = 1; + +-- 6. 图书评价 (Reviews) +-- 学生通常评价已借阅过的书 +INSERT INTO reviews (book_id, student_id, rating, content, review_time) VALUES +(5, 4, 5, '《人类简史》这本书太棒了,视角独特,值得一读!', now() - interval '1 day'), -- 赵六评价《人类简史》 +(1, 1, 4, '《深入理解计算机系统》有点难,但很有收获。', now()), -- 张三评价 +(2, 2, 5, '《数学之美》让我对数学有了新的认识。', now()); -- 李四评价 + +-- 尝试插入重复评价(应失败,因为有UNIQUE约束) +-- INSERT INTO reviews (book_id, student_id, rating, content, review_time) VALUES +-- (5, 4, 3, '第二次评价,内容一般。', now()); + +-- 7. 罚款记录 (Fines) +-- 部分罚款会由 trg_calc_fine 自动生成 (当逾期借阅记录更新为 'returned' 时) +-- 手动添加一些罚款记录: + +-- 罚款场景1: 图书损坏 (假设管理员A002处理) +INSERT INTO fines (student_id, amount, reason, status, issue_date, admin_id) VALUES +( (SELECT student_id FROM students WHERE stu_no = 'S2022003'), -- 王五 + 20.00, + '损坏图书《Python编程》(ISBN:9787115478850)', + 'unpaid', + current_date - interval '1 day', + (SELECT admin_id FROM admins WHERE emp_no = 'A002') +); + +-- 罚款场景2: 图书遗失 (假设管理员A002处理) +-- 对应 borrow_records 中周八遗失的《明朝那些事儿》 +INSERT INTO fines (student_id, amount, reason, status, issue_date, admin_id) VALUES +( (SELECT student_id FROM students WHERE stu_no = 'S2022006'), -- 周八 + (SELECT price FROM books WHERE isbn = '9787559620187'), -- 按书价赔偿 + '遗失图书《明朝那些事儿》(ISBN:9787559620187)', + 'unpaid', + current_date, + (SELECT admin_id FROM admins WHERE emp_no = 'A002') +); + +-- 罚款场景3: 之前的逾期罚款,已缴纳 +INSERT INTO fines (student_id, amount, reason, status, issue_date, admin_id) VALUES +( (SELECT student_id FROM students WHERE stu_no = 'S2021007'), -- 吴九 + 5.50, + '图书逾期11天 (旧记录)', + 'paid', + current_date - interval '60 days', + (SELECT admin_id FROM admins WHERE emp_no = 'A003') +); + + +---------------------------------------------------- +-- 测试触发器 trg_calc_fine 和 trg_freeze_account +---------------------------------------------------- +DO $$ +DECLARE + v_student_id_sunqi bigint; + v_borrow_id_sunqi bigint; + v_fine_per_day numeric; + v_days_overdue int; + v_expected_fine numeric; +BEGIN + -- 获取孙七的 student_id + SELECT student_id INTO v_student_id_sunqi FROM students WHERE stu_no = 'S2023005'; + -- 获取孙七之前插入的逾期借阅记录 (book_id=6, 追风筝的人) + SELECT borrow_id INTO v_borrow_id_sunqi + FROM borrow_records + WHERE student_id = v_student_id_sunqi AND book_id = (SELECT book_id FROM books WHERE isbn='9787532767406') AND status = 'overdue'; + + RAISE NOTICE '孙七(ID:%)的借阅记录ID: % 将被更新为已归还。', v_student_id_sunqi, v_borrow_id_sunqi; + + -- 模拟孙七归还之前逾期的书 (book_id=6, 《追风筝的人》) + -- 这将触发 trg_calc_fine + UPDATE borrow_records + SET status = 'returned', return_date = current_date + WHERE borrow_id = v_borrow_id_sunqi; + + RAISE NOTICE '已更新孙七的借阅记录。现在检查 fines 表是否自动产生罚款记录...'; + -- trg_calc_fine 会根据 borrow_records 中的 due_date 和 return_date 计算罚款并插入 fines 表 + -- 假设罚款汇率是 0.5/天,逾期5天,罚款应为 2.50 + -- (current_date - (current_date - interval '5 days')) * 0.50 + SELECT setting_value::numeric INTO v_fine_per_day FROM system_settings WHERE setting_key = 'fine_per_day'; + SELECT (current_date - (SELECT due_date FROM borrow_records WHERE borrow_id = v_borrow_id_sunqi)) INTO v_days_overdue; + v_expected_fine := v_days_overdue * v_fine_per_day; + RAISE NOTICE '预期罚款金额: % * % = %', v_days_overdue, v_fine_per_day, v_expected_fine; + + -- 检查孙七的账户状态是否因为罚款超过阈值 (默认20) 而被冻结 + -- trg_freeze_account 会在 fines 表 INSERT/UPDATE/DELETE 后触发 + RAISE NOTICE '检查孙七账户是否因罚款自动冻结...'; + -- 如果孙七的罚款 (例如 2.50) 没有超过 freeze_threshold (默认20.00),账户不会冻结。 + -- 我们再为孙七手动添加一笔大额罚款,使其总欠款超过阈值 + IF (SELECT sum(amount) FROM fines WHERE student_id = v_student_id_sunqi AND status = 'unpaid') < (SELECT setting_value::numeric FROM system_settings WHERE setting_key = 'freeze_threshold') THEN + RAISE NOTICE '孙七当前未付罚款未达冻结阈值,为其添加一笔大额罚款...'; + INSERT INTO fines (student_id, amount, reason, status, issue_date, admin_id) VALUES + (v_student_id_sunqi, 25.00, '严重损坏图书赔偿', 'unpaid', current_date, (SELECT admin_id FROM admins WHERE emp_no='A001')); + RAISE NOTICE '已为孙七添加大额罚款,再次检查账户状态。'; + END IF; + +END $$; + +-- 查询验证触发器效果 +RAISE NOTICE '---------- 查询验证 ----------'; + +RAISE NOTICE '1. 书籍可借数量和学生当前借阅量 (应由 trg_sync_book_student 更新):'; +SELECT book_id, title, total_copies, available_copies FROM books WHERE book_id <= 7; +SELECT student_id, name, max_borrow, current_borrow, account_status FROM students WHERE student_id <= 5; + +RAISE NOTICE '2. 孙七 (S2023005) 的罚款记录 (应有 trg_calc_fine 生成的逾期罚款 和 手动添加的罚款):'; +SELECT f.*, s.name as student_name +FROM fines f JOIN students s ON f.student_id = s.student_id +WHERE s.stu_no = 'S2023005'; + +RAISE NOTICE '3. 孙七 (S2023005) 的账户状态 (应由 trg_freeze_account 根据总欠款更新):'; +SELECT stu_no, name, account_status, current_borrow +FROM students +WHERE stu_no = 'S2023005'; + +RAISE NOTICE '4. 查看热门图书视图:'; +SELECT * FROM v_hot_books; + +RAISE NOTICE '5. 查看院系借阅统计视图:'; +SELECT * FROM v_dept_borrow_stats; + +RAISE NOTICE '6. 查看逾期详情视图:'; +SELECT od.*, b.title, s.name as student_name +FROM v_overdue_details od +JOIN books b ON od.book_id = b.book_id +JOIN students s ON od.student_id = s.student_id; + +RAISE NOTICE '测试数据插入完毕。'; \ No newline at end of file diff --git a/next.config.ts b/next.config.ts index e9ffa30..aebc082 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,6 +2,11 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { /* config options here */ -}; + images: { + remotePatterns: [new URL('https://picsum.photos/**')], + }, + +} + export default nextConfig; diff --git a/package.json b/package.json index 775a9dd..74008e5 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,25 @@ "lint": "next lint" }, "dependencies": { + "@hookform/resolvers": "^5.1.1", + "@tanstack/react-query": "^5.80.10", + "@tanstack/react-query-devtools": "^5.80.10", + "@types/bcryptjs": "^3.0.0", + "@types/pg": "^8.15.4", + "bcryptjs": "^3.0.2", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "drizzle-kit": "^0.31.1", + "drizzle-orm": "^0.44.2", + "lucide-react": "^0.520.0", + "next": "15.3.4", + "next-auth": "^4.24.11", + "pg": "^8.16.2", "react": "^19.0.0", "react-dom": "^19.0.0", - "next": "15.3.4" + "react-hook-form": "^7.58.1", + "tailwind-merge": "^3.3.1", + "zod": "^3.25.67" }, "devDependencies": { "typescript": "^5", diff --git a/sql/admin_query.sql b/sql/admin_query.sql new file mode 100644 index 0000000..3a60829 --- /dev/null +++ b/sql/admin_query.sql @@ -0,0 +1,469 @@ +-- 假设当前操作的管理员ID +-- 在实际应用中,这个ID会从登录会话中获取 +-- SELECT admin_id INTO v_admin_id FROM admins WHERE emp_no = 'A001'; -- 示例:获取张管理的ID +-- 为了简化示例,我们直接使用数字,假设 admin_id=1 是一个有效管理员 +-- 请根据你的 admins 表实际数据替换 + +---------------------------------------------------------------------- +-- (1). 图书信息管理(增删改查、批量导入) +---------------------------------------------------------------------- + +-- A. 新增图书 (Create) +INSERT INTO books (isbn, title, authors, publisher, publish_date, price, classification_no, location, total_copies, available_copies, description, cover_url) +VALUES +('9787121408881', 'Effective Java 中文版 (原书第3版)', ARRAY['Joshua Bloch'], '机械工业出版社', '2019-01-01', 119.00, 'TP312JA', 'A区3架5层', 5, 5, 'Java程序员必读的经典著作,包含90个实用条目。', 'http://example.com/cover_effective_java.jpg'); + +-- B. 查询图书 (Read) +-- B1. 按ISBN精确查询 +SELECT * FROM books WHERE isbn = '9787111624927'; + +-- B2. 按书名模糊查询 (使用 trigram 索引 idx_books_title_trgm) +SELECT * FROM books WHERE title % '计算机'; -- 查询包含“计算机”的 +SELECT * FROM books WHERE title ILIKE '%系统%'; -- 不区分大小写查询包含“系统”的 + +-- B3. 按作者查询 (使用 GIN 索引 idx_books_authors_gin) +SELECT * FROM books WHERE authors @> ARRAY['吴军']; -- 查询作者包含“吴军”的 + +-- B4. 按分类号查询 +SELECT * FROM books WHERE classification_no = 'TP301'; + +-- B5. 查看所有“正常”状态的图书 +SELECT * FROM books WHERE status = 'normal'; + +-- C. 修改图书信息 (Update) +-- 假设要修改 book_id 为 1 的图书信息 +UPDATE books +SET + price = 145.00, + location = 'A区2架3层 (更新)', + description = '计算机系统的经典之作,最新修订版。', + updated_at = now() +WHERE book_id = 1; -- 假设 book_id 为 1 的是《深入理解计算机系统》 + +-- 修改图书状态 (例如,将一本损坏的书修复后改为正常) +UPDATE books +SET status = 'normal', available_copies = available_copies + 1 -- 如果之前因损坏不可借 +WHERE isbn = '9787208159659' AND status = 'damaged'; -- 假设《三体全集》某本修复 + +-- D. 删除图书 (Delete) - 谨慎操作,通常建议逻辑删除或下架 +-- 注意:如果该书有关联的借阅记录、预约记录、评价记录,且外键设置了 ON DELETE CASCADE,则这些关联记录也会被删除。 +-- 如果设置了 ON DELETE RESTRICT (或默认),且存在关联记录,则删除会失败。 +-- 你的表结构中,borrow_records, reservations, reviews 都对 book_id 设置了 ON DELETE CASCADE。 +-- DELETE FROM books WHERE isbn = '9780132350884'; -- 删除已下架的《Clean Code》 + +-- 更好的方式是“下架” +UPDATE books +SET status = 'removed', available_copies = 0 +WHERE isbn = '9780132350884'; + +-- E. 批量导入图书 (Batch Import) +INSERT INTO books (isbn, title, authors, publisher, total_copies, available_copies) VALUES +('ISBN001', '批量导入书1', ARRAY['作者X'], '出版社A', 2, 2), +('ISBN002', '批量导入书2', ARRAY['作者Y', '作者Z'], '出版社B', 3, 3); + + +---------------------------------------------------------------------- +-- (2). 学生账户管理 +---------------------------------------------------------------------- +-- A. 新增学生账户 +INSERT INTO students (stu_no, name, gender, department, major, grade, class, phone, email, max_borrow) +VALUES +('S2024001', '刘新', 'M', '物理学院', '应用物理', '2024', '01班', '13600001111', 'liuxin@example.com', (SELECT setting_value::int FROM system_settings WHERE setting_key='max_borrow_default')); + +-- B. 查询学生账户 +-- B1. 按学号查询 +SELECT * FROM students WHERE stu_no = 'S2023001'; + +-- B2. 按姓名模糊查询 +SELECT * FROM students WHERE name LIKE '张%'; + +-- B3. 查询某院系所有学生 +SELECT * FROM students WHERE department = '计算机学院'; + +-- C. 修改学生账户信息 +-- C1. 修改学生联系方式 +UPDATE students +SET phone = '13511112222', email = 'new_zhangsan@example.com' +WHERE stu_no = 'S2023001'; + +-- C2. 修改学生账户状态 (例如:解挂、冻结/解冻 - 冻结通常由触发器完成,但管理员也可手动操作) +-- 解挂 +UPDATE students SET account_status = 'active' WHERE stu_no = 'S2023005' AND account_status = 'reported'; +-- 手动冻结 (如果业务需要) +UPDATE students SET account_status = 'frozen' WHERE stu_no = 'S2023001'; +-- 手动解冻 (例如,学生缴清罚款后,触发器未及时更新或有特殊情况) +-- 首先确保罚款已清或问题已解决 +-- UPDATE fines SET status = 'paid' WHERE student_id = (SELECT student_id FROM students WHERE stu_no = 'S2023005') AND status = 'unpaid'; +-- 然后(如果触发器没有将所有欠款清零后的账户自动激活) +-- UPDATE students SET account_status = 'active' WHERE stu_no = 'S2023005' AND account_status = 'frozen'; + +-- C3. 修改学生最大借阅量 +UPDATE students SET max_borrow = 12 WHERE stu_no = 'S2023001'; + + +---------------------------------------------------------------------- +-- (3). 处理借阅、归还、续借请求 +---------------------------------------------------------------------- +-- A. 处理借阅请求 (学生在前台操作,管理员后台处理或确认) +-- 假设学生 (stu_no='S2023002', 李四) 要借阅图书 (isbn='9787111624927', 深入理解计算机系统) +-- 前提检查 (通常在应用层完成,但也可在存储过程中封装): +-- 1. 图书可借: SELECT available_copies, status FROM books WHERE isbn = '9787111624927'; (available_copies > 0 and status = 'normal') +-- 2. 学生账户正常: SELECT account_status FROM students WHERE stu_no = 'S2023002'; (account_status = 'active') +-- 3. 学生未超借阅上限: SELECT current_borrow, max_borrow FROM students WHERE stu_no = 'S2023002'; (current_borrow < max_borrow) + +DO $$ +DECLARE + v_student_id bigint; + v_book_id bigint; + v_available_copies int; + v_book_status book_status_enum; + v_account_status acct_status_enum; + v_current_borrow int; + v_max_borrow int; + v_borrow_duration int; +BEGIN + SELECT student_id, account_status, current_borrow, max_borrow INTO v_student_id, v_account_status, v_current_borrow, v_max_borrow + FROM students WHERE stu_no = 'S2023002'; + + SELECT book_id, available_copies, status INTO v_book_id, v_available_copies, v_book_status + FROM books WHERE isbn = '9787111624927'; + + IF NOT FOUND THEN RAISE EXCEPTION '学生或图书不存在'; END IF; + + IF v_book_status != 'normal' THEN RAISE EXCEPTION '图书状态异常,不可借阅: %', v_book_status; END IF; + IF v_available_copies <= 0 THEN RAISE EXCEPTION '图书已无馆藏可借'; END IF; + IF v_account_status != 'active' THEN RAISE EXCEPTION '学生账户状态异常,不可借阅: %', v_account_status; END IF; + IF v_current_borrow >= v_max_borrow THEN RAISE EXCEPTION '已达到最大借阅量'; END IF; + + SELECT setting_value::int INTO v_borrow_duration FROM system_settings WHERE setting_key = 'borrow_duration_days'; + + INSERT INTO borrow_records (book_id, student_id, borrow_date, due_date, status) + VALUES (v_book_id, v_student_id, current_date, current_date + (v_borrow_duration || ' days')::interval, 'borrowed'); + + RAISE NOTICE '借阅成功! Book ID: %, Student ID: %', v_book_id, v_student_id; + -- 触发器 trg_sync_book_student 会自动更新 books.available_copies 和 students.current_borrow +END $$; + +-- B. 处理归还请求 +-- 假设要归还 borrow_id 为 (假设是1,根据实际情况查询得到) 的借阅记录 +-- SELECT borrow_id FROM borrow_records br JOIN students s ON br.student_id = s.student_id JOIN books b ON br.book_id = b.book_id +-- WHERE s.stu_no='S2023001' AND b.isbn='9787111624927' AND br.status IN ('borrowed', 'overdue'); (假设这是张三借的《深入理解计算机系统》) + +-- 获取特定借阅记录ID (例如,张三当前借阅的《深入理解计算机系统》) +DO $$ +DECLARE + v_borrow_id bigint; +BEGIN + SELECT br.borrow_id INTO v_borrow_id + FROM borrow_records br + JOIN students s ON br.student_id = s.student_id + JOIN books b ON br.book_id = b.book_id + WHERE s.stu_no = 'S2023001' AND b.isbn = '9787111624927' AND br.status IN ('borrowed', 'overdue') + ORDER BY br.borrow_date DESC LIMIT 1; -- 获取最近一次未还的 + + IF NOT FOUND THEN + RAISE NOTICE '未找到该学生对应的此书的当前借阅记录。'; + RETURN; + END IF; + + RAISE NOTICE '正在归还 borrow_id: %', v_borrow_id; + + UPDATE borrow_records + SET + status = 'returned', + return_date = current_date + WHERE borrow_id = v_borrow_id; + -- 触发器 trg_calc_fine 会在逾期归还时自动记录罚款 + -- 触发器 trg_sync_book_student 会自动更新 books.available_copies 和 students.current_borrow + RAISE NOTICE '归还操作完成。'; +END $$; + + +-- C. 处理续借请求 +-- 假设要续借 borrow_id 为 (假设是2,李四借的《数学之美》) 的借阅记录 +-- 续借条件检查 (应用层或存储过程): +-- 1. 图书未被其他人预约: SELECT 1 FROM reservations WHERE book_id = _book_id_ AND status = 'waiting'; (应为空) +-- 2. 未超最大续借次数: SELECT renew_times FROM borrow_records WHERE borrow_id = _borrow_id_; +-- (renew_times < (SELECT setting_value::int FROM system_settings WHERE setting_key = 'max_renew_times')) +-- 3. 学生账户正常,图书未逾期等。 + +DO $$ +DECLARE + v_borrow_id bigint; + v_book_id bigint; + v_renew_times int; + v_max_renew_times int; + v_is_reserved int; + v_borrow_duration int; + v_current_due_date date; + v_current_status borrow_status_enum; +BEGIN + -- 假设李四 (S2023002) 续借《数学之美》 (isbn='9787030598007') + SELECT br.borrow_id, br.book_id, br.renew_times, br.due_date, br.status + INTO v_borrow_id, v_book_id, v_renew_times, v_current_due_date, v_current_status + FROM borrow_records br + JOIN students s ON br.student_id = s.student_id + JOIN books b ON br.book_id = b.book_id + WHERE s.stu_no = 'S2023002' AND b.isbn = '9787030598007' AND br.status = 'borrowed' -- 通常只能续借未逾期的 + ORDER BY br.borrow_date DESC LIMIT 1; + + IF NOT FOUND THEN RAISE EXCEPTION '未找到该借阅记录或记录状态不符。'; END IF; + IF v_current_status = 'overdue' THEN RAISE EXCEPTION '图书已逾期,不可续借,请先处理逾期。'; END IF; + + + SELECT setting_value::int INTO v_max_renew_times FROM system_settings WHERE setting_key = 'max_renew_times'; + SELECT setting_value::int INTO v_borrow_duration FROM system_settings WHERE setting_key = 'borrow_duration_days'; + + IF v_renew_times >= v_max_renew_times THEN + RAISE EXCEPTION '已达到最大续借次数 (%)', v_max_renew_times; + END IF; + + SELECT count(*) INTO v_is_reserved FROM reservations + WHERE book_id = v_book_id AND status = 'waiting' + AND student_id != (SELECT student_id FROM students WHERE stu_no = 'S2023002'); -- 排除自己的预约 + + IF v_is_reserved > 0 THEN + RAISE EXCEPTION '该图书已被他人预约,不可续借。'; + END IF; + + UPDATE borrow_records + SET + due_date = v_current_due_date + (v_borrow_duration || ' days')::interval, -- 从当前应还日期开始延长 + renew_times = v_renew_times + 1 + WHERE borrow_id = v_borrow_id; + + RAISE NOTICE '续借成功! Borrow ID: %,新应还日期: %', v_borrow_id, (SELECT due_date FROM borrow_records WHERE borrow_id=v_borrow_id); +END $$; + + +---------------------------------------------------------------------- +-- (4). 管理预约队列 +---------------------------------------------------------------------- +-- A. 查看某本图书的预约队列 (例如 book_id = 7, Java核心技术) +SELECT r.reservation_id, r.reserve_date, r.status, s.stu_no, s.name as student_name, s.email +FROM reservations r +JOIN students s ON r.student_id = s.student_id +WHERE r.book_id = 7 AND r.status = 'waiting' -- 假设book_id 7是《Java核心技术 卷I》 +ORDER BY r.reserve_date ASC; -- 按预约日期升序,先预约的在前 + +-- B. 处理到书通知 (当一本被预约的书归还后) +-- 假设 book_id = 7 的书有副本归还,管理员通知队列中的第一个学生 +-- B1. 找到第一个等待的学生 +DO $$ +DECLARE + v_reservation_id bigint; + v_student_id bigint; + v_book_id int := 7; -- 假设是 Java 核心技术 + v_reservation_expiry_days int; +BEGIN + SELECT reservation_id, student_id INTO v_reservation_id, v_student_id + FROM reservations + WHERE book_id = v_book_id AND status = 'waiting' + ORDER BY reserve_date ASC + LIMIT 1; + + IF FOUND THEN + SELECT setting_value::int INTO v_reservation_expiry_days + FROM system_settings WHERE setting_key = 'reservation_expiry_days'; + + UPDATE reservations + SET status = 'available' -- (可以增加一个 notified_at timestamp 和 available_until timestamp 字段) + -- available_until = current_timestamp + (v_reservation_expiry_days || ' days')::interval -- (如果表有此字段) + WHERE reservation_id = v_reservation_id; + RAISE NOTICE '已通知学生ID % (预约ID %) 图书 % 可取。', v_student_id, v_reservation_id, v_book_id; + -- 实际应用中会发送邮件/短信 + ELSE + RAISE NOTICE '图书 % 没有等待中的预约。', v_book_id; + END IF; +END $$; + +-- C. 学生未在规定时间内取书,预约自动或手动过期 +UPDATE reservations +SET status = 'expired' +WHERE status = 'available' AND reservation_id = _reservation_id_ ; +-- AND available_until < current_timestamp; -- (如果表有 available_until 字段) + +-- D. 管理员取消某个预约 (例如,学生请求取消) +UPDATE reservations +SET status = 'cancelled' +WHERE reservation_id = _reservation_id_ ; -- 替换为实际预约ID + + +---------------------------------------------------------------------- +-- (5). 处理图书遗失、损坏等异常情况 +---------------------------------------------------------------------- +-- A. 处理图书遗失 +-- 假设学生 (stu_no='S2022006', 周八) 遗失了图书 (isbn='9787559620187', 明朝那些事儿) +-- A1. 更新借阅记录状态 (如果该书是被借阅后遗失的) +-- 你已经在测试数据中插入了一条 borrow_records 状态为 'lost' 的记录,这里假设是管理员新发现的遗失 +DO $$ +DECLARE + v_student_id bigint; + v_book_id bigint; + v_borrow_id bigint; + v_book_price numeric; + v_admin_id bigint := (SELECT admin_id FROM admins WHERE emp_no = 'A002'); -- 操作管理员ID +BEGIN + SELECT student_id INTO v_student_id FROM students WHERE stu_no = 'S2022006'; + SELECT book_id, price INTO v_book_id, v_book_price FROM books WHERE isbn = '9787559620187'; + + -- 查找该学生对这本书的未还借阅记录 + SELECT borrow_id INTO v_borrow_id + FROM borrow_records + WHERE student_id = v_student_id AND book_id = v_book_id AND status IN ('borrowed', 'overdue') + LIMIT 1; + + IF FOUND THEN + UPDATE borrow_records SET status = 'lost' WHERE borrow_id = v_borrow_id; + RAISE NOTICE '借阅记录 % 已更新为遗失。', v_borrow_id; + ELSE + RAISE NOTICE '未找到该学生对此书的当前借阅记录,可能是在馆藏中发现遗失。'; + END IF; + + -- A2. 更新图书信息状态和可借数量 (如果这本书之前是可借的) + -- total_copies 一般不变,除非彻底报废且不再补充 + UPDATE books + SET + status = 'lost', -- 如果整本书遗失 + available_copies = GREATEST(0, available_copies - 1) -- 确保可借不为负 + WHERE book_id = v_book_id AND status != 'lost'; -- 避免重复操作,如果已是lost就不再减available_copies + + -- A3. 生成罚款记录 (按书价赔偿) + INSERT INTO fines (student_id, amount, reason, status, issue_date, admin_id) + VALUES (v_student_id, v_book_price, '遗失图书《明朝那些事儿》(ISBN:9787559620187)', 'unpaid', current_date, v_admin_id); + RAISE NOTICE '遗失罚款已记录。'; + -- 触发器 trg_freeze_account 会检查是否需要冻结账户 + -- 触发器 trg_sync_book_student 会因为 borrow_records 更新而调整 current_borrow +END $$; + +-- B. 处理图书损坏 +-- 假设发现馆藏图书 (isbn='9787208159659', 三体全集) 有一本损坏,需要记录并可能罚款最后借阅人 +DO $$ +DECLARE + v_book_id bigint; + v_last_borrower_student_id bigint; + v_damage_fine_amount numeric := 20.00; -- 损坏赔偿金额 + v_admin_id bigint := (SELECT admin_id FROM admins WHERE emp_no = 'A002'); +BEGIN + SELECT book_id INTO v_book_id FROM books WHERE isbn = '9787208159659'; + + -- B1. 更新图书状态,如果损坏到不可借阅,减少可借数量 + UPDATE books + SET + status = 'damaged', + available_copies = GREATEST(0, available_copies - 1) -- 如果损坏导致不可借 + WHERE book_id = v_book_id; + RAISE NOTICE '图书 % 状态已更新为损坏。', v_book_id; + + -- B2. (可选) 查找最后借阅人并生成罚款 + SELECT student_id INTO v_last_borrower_student_id + FROM borrow_records + WHERE book_id = v_book_id AND return_date IS NOT NULL + ORDER BY return_date DESC + LIMIT 1; + + IF FOUND THEN + INSERT INTO fines (student_id, amount, reason, status, issue_date, admin_id) + VALUES (v_last_borrower_student_id, v_damage_fine_amount, '损坏图书《三体全集》(ISBN:9787208159659)', 'unpaid', current_date, v_admin_id); + RAISE NOTICE '已向最后借阅人 (学生ID: %) 记录损坏罚款。', v_last_borrower_student_id; + -- 触发器 trg_freeze_account + ELSE + RAISE NOTICE '未找到该书的最后借阅人,或为馆藏期间损坏。'; + END IF; +END $$; + +-- C. 图书下架 (例如,图书过于陈旧或残破不再流通) +UPDATE books +SET status = 'removed', available_copies = 0 +WHERE isbn = 'ISBN_TO_BE_REMOVED'; + + +---------------------------------------------------------------------- +-- (6). 设置和管理罚款规则 (主要通过 system_settings 表) +---------------------------------------------------------------------- +-- A. 查看当前罚款规则 +SELECT * FROM system_settings WHERE setting_key LIKE 'fine%'; + +-- B. 修改每日逾期罚款金额 +UPDATE system_settings +SET setting_value = '0.30' -- 修改为每天0.3元 +WHERE setting_key = 'fine_per_day'; + +-- C. 修改欠款冻结账户的阈值 +UPDATE system_settings +SET setting_value = '15.00' -- 修改为欠款15元冻结 +WHERE setting_key = 'freeze_threshold'; + +-- D. 添加新的罚款规则 (如果系统支持更复杂的规则,可能需要修改表结构或增加新表) +-- 当前设计下,罚款规则比较简单,主要就是每日费率。 + + +---------------------------------------------------------------------- +-- (7). 生成各类统计报表 (主要使用视图和聚合查询) +---------------------------------------------------------------------- +-- A. 查看热门图书 (借阅量前20,使用视图) +SELECT * FROM v_hot_books; + +-- B. 查看各院系借阅统计 (使用视图) +SELECT * FROM v_dept_borrow_stats; + +-- C. 查看图书逾期情况 (使用视图) +SELECT vd.*, b.title AS book_title, s.name AS student_name, s.phone AS student_phone +FROM v_overdue_details vd +JOIN books b ON vd.book_id = b.book_id +JOIN students s ON vd.student_id = s.student_id; + +-- D. 自定义报表:某一时段内各类图书的借阅次数 +SELECT + b.classification_no, + COUNT(br.borrow_id) AS borrow_count +FROM borrow_records br +JOIN books b ON br.book_id = b.book_id +WHERE br.borrow_date BETWEEN '2025-01-01' AND '2025-12-31' -- 示例时间段 +GROUP BY b.classification_no +ORDER BY borrow_count DESC; + +-- E. 自定义报表:每月借阅总量趋势 +SELECT + to_char(borrow_date, 'YYYY-MM') AS borrow_month, + COUNT(borrow_id) AS total_borrows +FROM borrow_records +GROUP BY borrow_month +ORDER BY borrow_month; + +-- F. 调用存储过程生成定期报表 (如果 sp_generate_circulation_stats 是设计为手动触发的) +-- SELECT sp_generate_circulation_stats(); +-- 然后查询报表数据 +-- SELECT * FROM circulation_stats ORDER BY stat_date DESC; + +---------------------------------------------------------------------- +-- (8). 系统参数设置 (主要通过 system_settings 表) +---------------------------------------------------------------------- +-- A. 查看所有系统参数 +SELECT * FROM system_settings; + +-- B. 修改默认最大借阅量 +UPDATE system_settings +SET setting_value = '8' +WHERE setting_key = 'max_borrow_default'; +-- 注意:这只影响未来新建学生账户的默认值,或在 sp_init_students 中使用此值批量更新。 +-- 已有学生的 max_borrow 需要单独修改,或在初始化脚本中覆盖。 + +-- C. 修改默认借阅期限(天数) +UPDATE system_settings +SET setting_value = '25' +WHERE setting_key = 'borrow_duration_days'; + +-- D. 修改最大续借次数 +UPDATE system_settings +SET setting_value = '1' +WHERE setting_key = 'max_renew_times'; + +-- E. 修改预约保留天数 +UPDATE system_settings +SET setting_value = '2' +WHERE setting_key = 'reservation_expiry_days'; + +-- F. 添加新的系统参数 (如果应用需要) +-- INSERT INTO system_settings (setting_key, setting_value) +-- VALUES ('new_feature_xyz_enabled', 'true'); \ No newline at end of file diff --git a/sql/advance_query.sql b/sql/advance_query.sql new file mode 100644 index 0000000..efea68d --- /dev/null +++ b/sql/advance_query.sql @@ -0,0 +1,30 @@ +-- 查询验证触发器效果 +---------- 查询验证 ---------- + +-- 1. 书籍可借数量和学生当前借阅量 (应由 trg_sync_book_student 更新): +SELECT book_id, title, total_copies, available_copies FROM books WHERE book_id <= 7; +SELECT student_id, name, max_borrow, current_borrow, account_status FROM students WHERE student_id <= 5; + +-- 2. 孙七 (S2023005) 的罚款记录 (应有 trg_calc_fine 生成的逾期罚款 和 手动添加的罚款): +SELECT f.*, s.name as student_name +FROM fines f JOIN students s ON f.student_id = s.student_id +WHERE s.stu_no = 'S2023005'; + +-- 3. 孙七 (S2023005) 的账户状态 (应由 trg_freeze_account 根据总欠款更新): +SELECT stu_no, name, account_status, current_borrow +FROM students +WHERE stu_no = 'S2023005'; + +-- 4. 查看热门图书视图: +SELECT * FROM v_hot_books; + +-- 5. 查看院系借阅统计视图: +SELECT * FROM v_dept_borrow_stats; + +-- 6. 查看逾期详情视图: +SELECT od.*, b.title, s.name as student_name +FROM v_overdue_details od +JOIN books b ON od.book_id = b.book_id +JOIN students s ON od.student_id = s.student_id; + +-- 测试数据插入完毕。 \ No newline at end of file diff --git a/sql/create_library.sql b/sql/create_library.sql new file mode 100644 index 0000000..c6df5bc --- /dev/null +++ b/sql/create_library.sql @@ -0,0 +1,346 @@ +/* ---------- 基础设置 ---------- */ +CREATE SCHEMA IF NOT EXISTS library; +SET search_path TO library,public; + +/* ---------- ENUM 类型 ---------- */ +CREATE TYPE gender_enum AS ENUM ('M','F','O'); +CREATE TYPE book_status_enum AS ENUM ('normal','lost','damaged','removed'); +CREATE TYPE borrow_status_enum AS ENUM ('borrowed','returned','overdue','lost'); +CREATE TYPE reserve_status_enum AS ENUM ('waiting','available','cancelled','expired'); +CREATE TYPE acct_status_enum AS ENUM ('active','reported','frozen'); + +/* ---------- 系统参数表 ---------- */ +CREATE TABLE system_settings ( + setting_key text PRIMARY KEY, + setting_value text NOT NULL +); +/* 罚款费率、冻结阈值等默认值 */ +INSERT INTO system_settings(setting_key,setting_value) VALUES + ('fine_per_day', '0.50'), -- 每本书每日罚款金额 + ('freeze_threshold', '20.00'), -- 欠款 ≥ 此值自动冻结 + ('max_borrow_default', '10'); -- 默认最大借阅量 + +/* ---------- 图书信息 ---------- */ +CREATE TABLE books ( + book_id bigserial PRIMARY KEY, + isbn varchar(13) UNIQUE NOT NULL, + title text NOT NULL, + authors text[] NOT NULL, + publisher text, + publish_date date, + price numeric(10,2), + classification_no varchar(64), + location varchar(255), + total_copies int NOT NULL DEFAULT 1 CHECK(total_copies>0), + available_copies int NOT NULL DEFAULT 1 CHECK(available_copies>=0), + status book_status_enum NOT NULL DEFAULT 'normal', + description text, + cover_url text, + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now() +); + +/* ---------- 帮助函数:按键名返回 int 型系统参数 ---------- */ +CREATE OR REPLACE FUNCTION library.get_setting_int(p_key text) +RETURNS int +LANGUAGE sql +STABLE -- ← 允许做 DEFAULT +AS $$ + SELECT setting_value::int + FROM library.system_settings + WHERE setting_key = p_key; +$$; + +/* ---------- 学生信息 ---------- */ +CREATE TABLE library.students ( + student_id bigserial PRIMARY KEY, + stu_no varchar(20) UNIQUE NOT NULL, -- 学号 + name varchar(64) NOT NULL, + gender gender_enum, + department varchar(128), + major varchar(128), + grade varchar(10), + class varchar(50), + phone varchar(20), + email varchar(255), + account_status acct_status_enum NOT NULL DEFAULT 'active', + max_borrow int NOT NULL + DEFAULT library.get_setting_int('max_borrow_default'), + current_borrow int NOT NULL DEFAULT 0, + created_at timestamptz DEFAULT now() +); + +/* ---------- 管理员信息 ---------- */ +CREATE TABLE admins ( + admin_id bigserial PRIMARY KEY, + emp_no varchar(20) UNIQUE NOT NULL, + name varchar(64) NOT NULL, + position varchar(64), + phone varchar(20), + privilege_lv int NOT NULL DEFAULT 1, + created_at timestamptz DEFAULT now() +); + +/* ---------- 借阅记录 ---------- */ +CREATE TABLE borrow_records ( + borrow_id bigserial PRIMARY KEY, + book_id bigint REFERENCES books(book_id) ON DELETE CASCADE, + student_id bigint REFERENCES students(student_id) ON DELETE CASCADE, + borrow_date date NOT NULL DEFAULT current_date, + due_date date NOT NULL, + return_date date, + renew_times int NOT NULL DEFAULT 0, + status borrow_status_enum NOT NULL DEFAULT 'borrowed', + fine_amount numeric(10,2) NOT NULL DEFAULT 0 +); + +/* ---------- 图书预约 ---------- */ +CREATE TABLE reservations ( + reservation_id bigserial PRIMARY KEY, + book_id bigint REFERENCES books(book_id) ON DELETE CASCADE, + student_id bigint REFERENCES students(student_id) ON DELETE CASCADE, + reserve_date date NOT NULL DEFAULT current_date, + status reserve_status_enum NOT NULL DEFAULT 'waiting' +); + +/* ---------- 图书评价 ---------- */ +CREATE TABLE reviews ( + review_id bigserial PRIMARY KEY, + book_id bigint REFERENCES books(book_id) ON DELETE CASCADE, + student_id bigint REFERENCES students(student_id) ON DELETE CASCADE, + rating int NOT NULL CHECK(rating BETWEEN 1 AND 5), + content text, + review_time timestamptz DEFAULT now(), + UNIQUE(book_id,student_id) -- 一人一书仅一次评价 +); + +/* ---------- 罚款记录 ---------- */ +CREATE TABLE fines ( + fine_id bigserial PRIMARY KEY, + student_id bigint REFERENCES students(student_id) ON DELETE CASCADE, + amount numeric(10,2) NOT NULL CHECK(amount>0), + reason text, + status varchar(10) NOT NULL CHECK(status IN ('unpaid','paid')), + issue_date date NOT NULL DEFAULT current_date, + admin_id bigint REFERENCES admins(admin_id) +); + +/* ---------- 拓展 ---------- */ +CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public; + +/* ---------- 索引 ---------- */ +CREATE INDEX idx_books_title_trgm + ON library.books USING gin (title gin_trgm_ops); +CREATE INDEX idx_books_authors_gin + ON library.books USING gin (authors); +CREATE INDEX idx_br_student_status ON borrow_records(student_id,status); +CREATE INDEX idx_reserve_book_wait ON reservations(status,book_id); +CREATE INDEX idx_fines_student_unpaid ON fines(student_id) WHERE status='unpaid'; + +/* 要使用 trigram GIN 索引需启用扩展 */ +CREATE EXTENSION IF NOT EXISTS pg_trgm; + +/* ---------- 触发器函数 ---------- */ + +/* 1. 更新可借数量 & 学生当前借阅量 */ +CREATE OR REPLACE FUNCTION trg_sync_book_student() RETURNS TRIGGER AS $$ +BEGIN + -- 更新图书可借册数 + UPDATE books SET available_copies = total_copies - + (SELECT count(*) FROM borrow_records + WHERE book_id = COALESCE(NEW.book_id,OLD.book_id) + AND status IN ('borrowed','overdue')) + WHERE book_id = COALESCE(NEW.book_id,OLD.book_id); + + -- 更新学生当前借阅量 + UPDATE students SET current_borrow = + (SELECT count(*) FROM borrow_records + WHERE student_id = COALESCE(NEW.student_id,OLD.student_id) + AND status IN ('borrowed','overdue')) + WHERE student_id = COALESCE(NEW.student_id,OLD.student_id); + + RETURN NULL; +END; $$ LANGUAGE plpgsql; + +/* 2. 归还时自动计算并记账罚款 */ +CREATE OR REPLACE FUNCTION trg_calc_fine() RETURNS TRIGGER AS $$ +DECLARE + days_overdue int; + rate numeric(10,2) := (SELECT setting_value::numeric FROM system_settings WHERE setting_key='fine_per_day'); +BEGIN + IF TG_OP='UPDATE' AND NEW.status='returned' AND OLD.status IN ('borrowed','overdue') THEN + days_overdue := GREATEST((NEW.return_date - NEW.due_date),0); + IF days_overdue > 0 THEN + INSERT INTO fines(student_id,amount,reason,status,admin_id) + VALUES (NEW.student_id, days_overdue*rate, + format('Overdue %s days for borrow_id=%s',days_overdue,NEW.borrow_id), + 'unpaid', NULL); + END IF; + END IF; + RETURN NEW; +END; $$ LANGUAGE plpgsql; + +/* 3. 欠款超阈值自动冻结账户 */ +CREATE OR REPLACE FUNCTION library.trg_freeze_account() RETURNS TRIGGER AS $$ +DECLARE + total_unpaid numeric(10,2); + threshold numeric(10,2); + v_relevant_student_id bigint; + v_current_status library.acct_status_enum; + v_new_status library.acct_status_enum; +BEGIN + -- 1. 获取最新的冻结阈值 + SELECT setting_value::numeric INTO threshold + FROM library.system_settings + WHERE setting_key = 'freeze_threshold'; + + IF NOT FOUND THEN + RAISE WARNING 'System setting "freeze_threshold" not found. Account status will not be updated by trigger.'; + RETURN NULL; -- 或者 RAISE EXCEPTION; + END IF; + + -- 2. 确定相关的学生ID + IF TG_OP = 'DELETE' THEN + v_relevant_student_id := OLD.student_id; + ELSE -- INSERT or UPDATE + v_relevant_student_id := NEW.student_id; + END IF; + + -- 如果没有相关的学生ID(理论上不应发生,因为student_id是FK且NOT NULL),则退出 + IF v_relevant_student_id IS NULL THEN + RAISE WARNING 'trg_freeze_account: No relevant student_id found. TG_OP: %', TG_OP; + RETURN NULL; + END IF; + + -- 3. 计算该学生未缴罚款总额 + SELECT COALESCE(sum(amount),0) INTO total_unpaid + FROM library.fines + WHERE student_id = v_relevant_student_id AND status = 'unpaid'; + + -- 4. 获取学生当前账户状态 + SELECT account_status INTO v_current_status + FROM library.students + WHERE student_id = v_relevant_student_id; + + -- 5. 根据罚款确定新的目标状态 + -- 注意:原始逻辑是,如果欠款低于阈值,则账户变为 'active'。 + -- 这意味着如果账户之前是 'reported'(挂失),且欠款低于阈值,它也会被此触发器改为 'active'。 + -- 如果希望 'reported' 状态不受此罚款逻辑影响(除非因欠款被冻结),则需要更复杂的判断。 + -- 此处保持与你原触发器相似的逻辑,仅修复类型问题并优化。 + IF total_unpaid >= threshold THEN + v_new_status := 'frozen'::library.acct_status_enum; + ELSE + -- 如果当前已经是 'frozen',则解冻为 'active' + -- 如果当前不是 'frozen' (比如是 'active' 或 'reported'),且欠款未超限,则应保持其原状态, + -- 而不是都强制变为 'active'。 + -- 为了更安全地处理 'reported' 状态,我们修改这里的逻辑: + IF v_current_status = 'frozen'::library.acct_status_enum THEN + v_new_status := 'active'::library.acct_status_enum; + ELSE + v_new_status := v_current_status; -- 保持现有状态 (active 或 reported) + END IF; + END IF; + + -- 6. 如果计算出的新状态与当前状态不同,则更新学生账户状态 + IF v_current_status IS DISTINCT FROM v_new_status THEN + UPDATE library.students + SET account_status = v_new_status + WHERE student_id = v_relevant_student_id; + END IF; + + RETURN NULL; -- AFTER 触发器通常返回 NULL +END; +$$ LANGUAGE plpgsql; + +/* ---------- 触发器绑定 ---------- */ +CREATE TRIGGER trg_borrow_sync_aiud +AFTER INSERT OR UPDATE OR DELETE ON borrow_records +FOR EACH ROW EXECUTE FUNCTION trg_sync_book_student(); + +CREATE TRIGGER trg_borrow_calc_fine_upd +AFTER UPDATE ON borrow_records +FOR EACH ROW EXECUTE FUNCTION trg_calc_fine(); + +CREATE TRIGGER trg_fine_freeze_aiud +AFTER INSERT OR UPDATE OR DELETE ON fines +FOR EACH ROW EXECUTE FUNCTION trg_freeze_account(); + +/* ---------- 视图 ---------- */ + +/* (1) 当前热门图书:借阅量前 20 */ +CREATE OR REPLACE VIEW v_hot_books AS +SELECT b.book_id, b.isbn, b.title, + COUNT(br.borrow_id) AS borrow_cnt +FROM books b +JOIN borrow_records br ON br.book_id = b.book_id +GROUP BY b.book_id +ORDER BY borrow_cnt DESC +LIMIT 20; + +/* (2) 各院系借阅统计 */ +CREATE OR REPLACE VIEW v_dept_borrow_stats AS +SELECT s.department, + COUNT(br.borrow_id) AS total_borrows, + COUNT(DISTINCT br.student_id) AS unique_readers +FROM borrow_records br +JOIN students s ON s.student_id = br.student_id +GROUP BY s.department +ORDER BY total_borrows DESC; + +/* (3) 图书逾期情况 */ +CREATE OR REPLACE VIEW v_overdue_details AS +SELECT br.borrow_id, br.book_id, br.student_id, + br.due_date, CURRENT_DATE - br.due_date AS days_overdue +FROM borrow_records br +WHERE br.status='overdue'; + +/* ---------- 存储过程(plpgsql 函数) ---------- */ + +/* 1. 学期初批量初始化学生账户 + - 示例:EXECUTE library.sp_init_students('2025-Fall'); +*/ +CREATE OR REPLACE FUNCTION sp_init_students(term_code text) +RETURNS void LANGUAGE plpgsql AS $$ +BEGIN + -- 示例逻辑:所有学生 current_borrow 清零、状态激活 + UPDATE students SET current_borrow=0, account_status='active'; + RAISE NOTICE 'Students initialized for %', term_code; +END; $$; + +/* 2. 定期生成图书流通统计(存入自定义表) */ +CREATE TABLE IF NOT EXISTS circulation_stats ( + stat_date date PRIMARY KEY, + total_borrows bigint, + unique_readers bigint, + created_at timestamptz DEFAULT now() +); + +CREATE OR REPLACE FUNCTION sp_generate_circulation_stats() +RETURNS void LANGUAGE plpgsql AS $$ +BEGIN + INSERT INTO circulation_stats(stat_date,total_borrows,unique_readers) + SELECT CURRENT_DATE, + (SELECT COUNT(*) FROM borrow_records WHERE borrow_date=CURRENT_DATE), + (SELECT COUNT(DISTINCT student_id) FROM borrow_records WHERE borrow_date=CURRENT_DATE); +END; $$; + +/* 3. 自动发送逾期提醒(示例写入提醒表) */ +CREATE TABLE IF NOT EXISTS overdue_notices ( + notice_id bigserial PRIMARY KEY, + borrow_id bigint, + student_id bigint, + notice_time timestamptz DEFAULT now() +); + +CREATE OR REPLACE FUNCTION sp_send_overdue_notices() +RETURNS void LANGUAGE plpgsql AS $$ +BEGIN + INSERT INTO overdue_notices(borrow_id,student_id) + SELECT br.borrow_id, br.student_id + FROM borrow_records br + WHERE br.status='overdue' + AND NOT EXISTS (SELECT 1 FROM overdue_notices onot WHERE onot.borrow_id=br.borrow_id); +END; $$; + +/* ---------- 完成 ---------- */ +COMMENT ON SCHEMA library IS '智能图书管理系统数据库架构(2025-06-21)'; \ No newline at end of file diff --git a/sql/example_data.sql b/sql/example_data.sql new file mode 100644 index 0000000..5b79c0a --- /dev/null +++ b/sql/example_data.sql @@ -0,0 +1,208 @@ +SET search_path TO library, public; + +-- 清理可能存在的旧数据(可选,测试时方便重跑) +-- 请注意,CASCADE 会删除关联数据,请谨慎在生产环境使用 +DELETE FROM fines; +DELETE FROM reviews; +DELETE FROM reservations; +DELETE FROM borrow_records; +DELETE FROM admins; +DELETE FROM students; +DELETE FROM books; +DELETE FROM system_settings WHERE setting_key NOT IN ('fine_per_day', 'freeze_threshold', 'max_borrow_default'); -- 保留核心设置 + +-- 0. 系统参数 (你已插入,这里可以补充更多,如果需要) +INSERT INTO system_settings(setting_key, setting_value) VALUES +('max_renew_times', '2'), -- 最大续借次数 +('borrow_duration_days', '30'), -- 默认借阅时长(天) +('reservation_expiry_days', '3'), -- 预约保留天数 +('admin_default_password_hash', 'xxxx'); -- 示例:管理员默认密码哈希 +-- ON CONFLICT (setting_key) DO UPDATE SET setting_value = EXCLUDED.setting_value; -- 如果要允许更新 + +-- 1. 管理员信息 (Admins) +INSERT INTO admins (emp_no, name, position, phone, privilege_lv) VALUES +('A001', '张管理', '馆长', '13800138000', 0), -- 最高权限 +('A002', '李协助', '图书管理员', '13900139000', 1), +('A003', '王登记', '流通部职员', '13700137000', 2); + +-- 2. 图书信息 (Books) +-- 注意: available_copies 初始设为 total_copies,后续借阅会通过触发器减少 +INSERT INTO books (isbn, title, authors, publisher, publish_date, price, classification_no, location, total_copies, available_copies, status, description, cover_url) VALUES +('9787111624927', '深入理解计算机系统', ARRAY['Randal E. Bryant', 'David R. O''Hallaron'], '机械工业出版社', '2019-05-01', 139.00, 'TP301', 'A区2架3层', 10, 10, 'normal', '计算机系统的经典之作。', 'http://example.com/cover1.jpg'), +('9787030598007', '数学之美', ARRAY['吴军'], '科学出版社', '2020-01-01', 68.00, 'O1', 'B区1架1层', 15, 15, 'normal', '通俗易懂的数学科普读物。', 'http://example.com/cover2.jpg'), +('9787544270878', '百年孤独', ARRAY['加西亚·马尔克斯'], '南海出版公司', '2011-06-01', 39.50, 'I775.45', 'C区5架2层', 8, 8, 'normal', '魔幻现实主义文学的代表作。', 'http://example.com/cover3.jpg'), +('9787115478850', 'Python编程:从入门到实践', ARRAY['Eric Matthes'], '人民邮电出版社', '2018-03-01', 89.00, 'TP312PY', 'A区3架1层', 12, 12, 'normal', 'Python入门畅销书。', 'http://example.com/cover4.jpg'), +('9787508647009', '人类简史:从动物到上帝', ARRAY['尤瓦尔·赫拉利'], '中信出版社', '2014-11-01', 68.00, 'K02', 'D区1架1层', 20, 20, 'normal', '一部宏大的人类发展史。', 'http://example.com/cover5.jpg'), +('9787532767406', '追风筝的人', ARRAY['卡勒德·胡赛尼'], '上海人民出版社', '2006-05-01', 29.00, 'I712.45', 'C区4架3层', 7, 7, 'normal', '关于爱、友谊、背叛和救赎的故事。', 'http://example.com/cover6.jpg'), +('9787121362308', 'Java核心技术 卷I', ARRAY['Cay S. Horstmann'], '电子工业出版社', '2019-06-01', 128.00, 'TP312JA', 'A区3架2层', 5, 0, 'normal', 'Java经典教材,目前全部被借出,用于测试预约。', 'http://example.com/cover7.jpg'), -- 特意设置 available_copies 为 0 +('9787208159659', '三体全集', ARRAY['刘慈欣'], '重庆出版社', '2019-01-01', 168.00, 'I247.5', 'C区6架1层', 3, 3, 'damaged', '科幻巨作,其中一本有损坏。', 'http://example.com/cover8.jpg'), +('9787559620187', '明朝那些事儿 (套装全7册)', ARRAY['当年明月'], '北京联合出版公司', '2017-08-01', 258.00, 'K248', 'D区2架1层', 6, 6, 'normal', '通俗易懂的明史。', 'http://example.com/cover9.jpg'), +('9787111600815', '算法导论 (原书第3版)', ARRAY['Thomas H. Cormen'], '机械工业出版社', '2012-12-01', 128.00, 'TP301.6', 'A区2架4层', 9, 9, 'normal', '算法领域的圣经。', 'http://example.com/cover10.jpg'), +('9780132350884', 'Clean Code', ARRAY['Robert C. Martin'], 'Prentice Hall', '2008-08-01', 50.00, 'TP311.1', 'A区1架5层', 4, 4, 'removed', '一本已下架的书,测试状态。', 'http://example.com/cover11.jpg'); + + +-- 3. 学生信息 (Students) +-- max_borrow 使用默认值(通过 get_setting_int('max_borrow_default') 获取,即10) +-- current_borrow 初始为0,会由触发器更新 +INSERT INTO students (stu_no, name, gender, department, major, grade, class, phone, email, account_status) VALUES +('S2023001', '张三', 'M', '计算机学院', '软件工程', '2023', '01班', '13512345671', 'zhangsan@example.com', 'active'), +('S2023002', '李四', 'F', '计算机学院', '人工智能', '2023', '02班', '13512345672', 'lisi@example.com', 'active'), +('S2022003', '王五', 'M', '外国语学院', '英语', '2022', '01班', '13512345673', 'wangwu@example.com', 'active'), +('S2021004', '赵六', 'F', '经济管理学院', '工商管理', '2021', '03班', '13512345674', 'zhaoliu@example.com', 'active'), +('S2023005', '孙七', 'M', '计算机学院', '软件工程', '2023', '01班', '13512345675', 'sunqi@example.com', 'reported'), -- 测试挂失状态 +('S2022006', '周八', 'F', '人文学院', '历史学', '2022', '02班', '13512345676', 'zhouba@example.com', 'active'), +('S2021007', '吴九', 'M', '理学院', '数学与应用数学', '2021', '01班', '13512345677', 'wujiu@example.com', 'active'), +('S2023008', '郑十', 'F', '计算机学院', '网络工程', '2023', '03班', '13512345678', 'zhengshi@example.com', 'active'); + +-- 修改一个学生的 max_borrow,测试非默认值 +UPDATE students SET max_borrow = 15 WHERE stu_no = 'S2023001'; +UPDATE students SET max_borrow = 5 WHERE stu_no = 'S2021004'; -- 用于测试借阅上限 + +-- 4. 借阅记录 (Borrow Records) +-- book_id 和 student_id 需要引用已存在的 ID +-- due_date = borrow_date + (SELECT setting_value::int FROM system_settings WHERE setting_key = 'borrow_duration_days') +-- fine_amount 初始为0, 逾期归还将由触发器在 fines 表中记录罚款 +-- 触发器 trg_sync_book_student 会在每次插入/更新/删除后执行 + +-- 获取 book_id 和 student_id 的示例 (实际使用时,你可能需要根据书名/学号查询得到) +-- SELECT book_id FROM books WHERE isbn = '9787111624927'; -- 1 +-- SELECT student_id FROM students WHERE stu_no = 'S2023001'; -- 1 + +-- 借阅场景 1: 正常借出,未到期 +INSERT INTO borrow_records (book_id, student_id, borrow_date, due_date, status) VALUES +(1, 1, current_date - interval '10 days', current_date - interval '10 days' + interval '30 days', 'borrowed'), -- 张三借《深入理解计算机系统》 +(2, 2, current_date - interval '5 days', current_date - interval '5 days' + interval '30 days', 'borrowed'); -- 李四借《数学之美》 + +-- 借阅场景 2: 正常借出,今天到期 +INSERT INTO borrow_records (book_id, student_id, borrow_date, due_date, status) VALUES +(3, 1, current_date - interval '30 days', current_date, 'borrowed'); -- 张三借《百年孤独》,今天到期 + +-- 借阅场景 3: 已逾期,未归还 +INSERT INTO borrow_records (book_id, student_id, borrow_date, due_date, status) VALUES +(4, 3, current_date - interval '40 days', current_date - interval '10 days', 'overdue'); -- 王五借《Python编程》,已逾期10天 + +-- 借阅场景 4: 正常归还 (触发器 trg_calc_fine 不会产生罚款) +INSERT INTO borrow_records (book_id, student_id, borrow_date, due_date, return_date, status) VALUES +(5, 4, current_date - interval '20 days', current_date + interval '10 days', current_date - interval '2 days', 'returned'); -- 赵六借《人类简史》,已按时归还 + +-- 借阅场景 5: 逾期归还 (为了测试 trg_calc_fine,先插入为逾期,再UPDATE为归还) +-- 步骤A: 先插入一条记录,状态为 'overdue' (或者 'borrowed' 但实际已过 due_date) +INSERT INTO borrow_records (book_id, student_id, borrow_date, due_date, status) +VALUES (6, 5, current_date - interval '35 days', current_date - interval '5 days', 'overdue'); -- 孙七借《追风筝的人》 +-- 记录下这条 borrow_id (假设是 6,根据实际情况调整) +-- 步骤B: 更新这条记录为 'returned',这将触发 trg_calc_fine +-- UPDATE borrow_records +-- SET status = 'returned', return_date = current_date +-- WHERE borrow_id = (SELECT borrow_id FROM borrow_records WHERE student_id=5 AND book_id=6 AND status='overdue'); +-- (该update放到下面演示触发器部分) + +-- 借阅场景 6: 书籍遗失 +INSERT INTO borrow_records (book_id, student_id, borrow_date, due_date, status) VALUES +(9, 6, current_date - interval '15 days', current_date + interval '15 days', 'lost'); -- 周八借《明朝那些事儿》,遗失 + + +-- 5. 图书预约 (Reservations) +-- 只能预约 available_copies = 0 的书 +-- (book_id=7, 'Java核心技术 卷I', available_copies 初始为0) +INSERT INTO reservations (book_id, student_id, reserve_date, status) VALUES +(7, 1, current_date - interval '2 days', 'waiting'), -- 张三预约《Java核心技术》 +(7, 2, current_date - interval '1 day', 'waiting'); -- 李四预约《Java核心技术》 + +-- 假设一本被预约的书有归还了,管理员将其状态改为可取 +-- UPDATE reservations SET status = 'available' WHERE book_id = 7 AND student_id = 1; + +-- 6. 图书评价 (Reviews) +-- 学生通常评价已借阅过的书 +INSERT INTO reviews (book_id, student_id, rating, content, review_time) VALUES +(5, 4, 5, '《人类简史》这本书太棒了,视角独特,值得一读!', now() - interval '1 day'), -- 赵六评价《人类简史》 +(1, 1, 4, '《深入理解计算机系统》有点难,但很有收获。', now()), -- 张三评价 +(2, 2, 5, '《数学之美》让我对数学有了新的认识。', now()); -- 李四评价 + +-- 尝试插入重复评价(应失败,因为有UNIQUE约束) +-- INSERT INTO reviews (book_id, student_id, rating, content, review_time) VALUES +-- (5, 4, 3, '第二次评价,内容一般。', now()); + +-- 7. 罚款记录 (Fines) +-- 部分罚款会由 trg_calc_fine 自动生成 (当逾期借阅记录更新为 'returned' 时) +-- 手动添加一些罚款记录: + +-- 罚款场景1: 图书损坏 (假设管理员A002处理) +INSERT INTO fines (student_id, amount, reason, status, issue_date, admin_id) VALUES +( (SELECT student_id FROM students WHERE stu_no = 'S2022003'), -- 王五 + 20.00, + '损坏图书《Python编程》(ISBN:9787115478850)', + 'unpaid', + current_date - interval '1 day', + (SELECT admin_id FROM admins WHERE emp_no = 'A002') +); + +-- 罚款场景2: 图书遗失 (假设管理员A002处理) +-- 对应 borrow_records 中周八遗失的《明朝那些事儿》 +INSERT INTO fines (student_id, amount, reason, status, issue_date, admin_id) VALUES +( (SELECT student_id FROM students WHERE stu_no = 'S2022006'), -- 周八 + (SELECT price FROM books WHERE isbn = '9787559620187'), -- 按书价赔偿 + '遗失图书《明朝那些事儿》(ISBN:9787559620187)', + 'unpaid', + current_date, + (SELECT admin_id FROM admins WHERE emp_no = 'A002') +); + +-- 罚款场景3: 之前的逾期罚款,已缴纳 +INSERT INTO fines (student_id, amount, reason, status, issue_date, admin_id) VALUES +( (SELECT student_id FROM students WHERE stu_no = 'S2021007'), -- 吴九 + 5.50, + '图书逾期11天 (旧记录)', + 'paid', + current_date - interval '60 days', + (SELECT admin_id FROM admins WHERE emp_no = 'A003') +); + + +---------------------------------------------------- +-- 测试触发器 trg_calc_fine 和 trg_freeze_account +---------------------------------------------------- +DO $$ +DECLARE + v_student_id_sunqi bigint; + v_borrow_id_sunqi bigint; + v_fine_per_day numeric; + v_days_overdue int; + v_expected_fine numeric; +BEGIN + -- 获取孙七的 student_id + SELECT student_id INTO v_student_id_sunqi FROM students WHERE stu_no = 'S2023005'; + -- 获取孙七之前插入的逾期借阅记录 (book_id=6, 追风筝的人) + SELECT borrow_id INTO v_borrow_id_sunqi + FROM borrow_records + WHERE student_id = v_student_id_sunqi AND book_id = (SELECT book_id FROM books WHERE isbn='9787532767406') AND status = 'overdue'; + + RAISE NOTICE '孙七(ID:%)的借阅记录ID: % 将被更新为已归还。', v_student_id_sunqi, v_borrow_id_sunqi; + + -- 模拟孙七归还之前逾期的书 (book_id=6, 《追风筝的人》) + -- 这将触发 trg_calc_fine + UPDATE borrow_records + SET status = 'returned', return_date = current_date + WHERE borrow_id = v_borrow_id_sunqi; + + RAISE NOTICE '已更新孙七的借阅记录。现在检查 fines 表是否自动产生罚款记录...'; + -- trg_calc_fine 会根据 borrow_records 中的 due_date 和 return_date 计算罚款并插入 fines 表 + -- 假设罚款汇率是 0.5/天,逾期5天,罚款应为 2.50 + -- (current_date - (current_date - interval '5 days')) * 0.50 + SELECT setting_value::numeric INTO v_fine_per_day FROM system_settings WHERE setting_key = 'fine_per_day'; + SELECT (current_date - (SELECT due_date FROM borrow_records WHERE borrow_id = v_borrow_id_sunqi)) INTO v_days_overdue; + v_expected_fine := v_days_overdue * v_fine_per_day; + RAISE NOTICE '预期罚款金额: % * % = %', v_days_overdue, v_fine_per_day, v_expected_fine; + + -- 检查孙七的账户状态是否因为罚款超过阈值 (默认20) 而被冻结 + -- trg_freeze_account 会在 fines 表 INSERT/UPDATE/DELETE 后触发 + RAISE NOTICE '检查孙七账户是否因罚款自动冻结...'; + -- 如果孙七的罚款 (例如 2.50) 没有超过 freeze_threshold (默认20.00),账户不会冻结。 + -- 我们再为孙七手动添加一笔大额罚款,使其总欠款超过阈值 + IF (SELECT sum(amount) FROM fines WHERE student_id = v_student_id_sunqi AND status = 'unpaid') < (SELECT setting_value::numeric FROM system_settings WHERE setting_key = 'freeze_threshold') THEN + RAISE NOTICE '孙七当前未付罚款未达冻结阈值,为其添加一笔大额罚款...'; + INSERT INTO fines (student_id, amount, reason, status, issue_date, admin_id) VALUES + (v_student_id_sunqi, 25.00, '严重损坏图书赔偿', 'unpaid', current_date, (SELECT admin_id FROM admins WHERE emp_no='A001')); + RAISE NOTICE '已为孙七添加大额罚款,再次检查账户状态。'; + END IF; + +END $$; diff --git a/sql/requirements.md b/sql/requirements.md new file mode 100644 index 0000000..2e1e141 --- /dev/null +++ b/sql/requirements.md @@ -0,0 +1,42 @@ +1. 数据库设计 +设计一个关系型数据库,可包含以下实体及属性: +(1). 图书信息:ISBN、书名、作者、出版社、出版日期、价格、分类号、馆藏位置、总册数、可借册数、图书状态(正常/遗失/损坏/下架等)、简介、封面图片URL +(2). 学生信息:学号、姓名、性别、院系、专业、年级、班级、联系方式、邮箱、账户状态(正常/挂失/冻结)、最大借阅量、当前借阅量 +(3). 借阅记录:借阅ID、图书ID、学号、借出日期、应还日期、实际归还日期、续借次数、借阅状态(借出/已还/逾期/遗失等)、罚款金额 +(4). 管理员信息:工号、姓名、职位、联系方式、权限等级 +(5). 图书预约:预约ID、图书ID、学号、预约日期、预约状态(等待/可取/取消/过期) +(6). 图书评价:评价ID、图书ID、学号、评分、评论内容、评论时间 +(7). 罚款记录:记录ID、学号、金额、产生原因、缴纳状态、产生日期、处理管理员 +2. 功能需求 +学生功能: +(1). 多条件组合查询图书(按书名、作者、分类、ISBN等) +(2). 查看图书详情及当前可借状态 +(3). 在线预约热门图书(当所有副本都被借出时) +(4). 查看个人借阅历史、当前借阅情况和预约状态 +(5). 在线续借图书(有限制条件) +(6). 查看和缴纳罚款 +(7). 对已借阅图书进行评分和评论 +(8). 查看图书推荐(如基于热门被借阅图书) +管理员功能: +(1). 图书信息管理(增删改查、批量导入) +(2). 学生账户管理 +(3). 处理借阅、归还、续借请求 +(4). 管理预约队列 +(5). 处理图书遗失、损坏等异常情况 +(6). 设置和管理罚款规则 +(7). 生成各类统计报表(借阅量统计、热门图书、逾期分析等) +(8). 系统参数设置(如最大借阅量、借阅期限等) +3. 高级功能要求 +(1). 设计触发器实现以下功能: +o 自动更新图书的可借数量 +o 自动计算并记录逾期罚款 +o 学生账户状态自动更新(如欠款超过阈值自动冻结) +(2). 设计存储过程实现: +o 批量处理学期初的学生账户初始化 +o 定期生成图书流通统计报表 +o 自动发送逾期提醒 +(3). 设计视图: +o 当前热门图书视图(借阅量前20) +o 各院系借阅统计视图 +o 图书逾期情况视图 +(4). 设计索引优化查询性能(选做) diff --git a/sql/user_query.sql b/sql/user_query.sql new file mode 100644 index 0000000..cb3754c --- /dev/null +++ b/sql/user_query.sql @@ -0,0 +1,340 @@ +-- 假设当前操作的学生是 张三 (学号 S2023001),我们先获取其 student_id +-- 在实际应用中,这个 ID 通常来自登录会话 +DO $$ +DECLARE + v_student_id bigint; +BEGIN + SELECT student_id INTO v_student_id FROM students WHERE stu_no = 'S2023001'; + -- 将 v_student_id 设为会话级变量,方便后续查询使用 + -- 注意: 这是一种在 psql 或特定SQL工具中设置变量的方式, + -- 在应用代码中,你会直接使用从会话中获取的 student_id 变量。 + EXECUTE 'SET app.current_student_id = ' || v_student_id; +EXCEPTION + WHEN NO_DATA_FOUND THEN + RAISE NOTICE '学生 S2023001 未找到。请确保测试数据已插入。'; + -- 或者你可以在这里直接硬编码一个存在的 student_id 用于测试 + -- EXECUTE 'SET app.current_student_id = 1'; +END $$; + +-- 如果上面的 DO $$ 块由于环境限制无法执行 SET app.current_student_id, +-- 后续查询中请手动替换 current_setting('app.current_student_id')::bigint 为实际的 student_id (例如 1) + +-- (1). 多条件组合查询图书 +RAISE NOTICE '---------- (1) 多条件组合查询图书 ----------'; + +-- 按书名模糊查询 (使用 pg_trgm GIN 索引) +RAISE NOTICE '-- 按书名模糊查询 "计算机":'; +SELECT book_id, isbn, title, authors, available_copies +FROM books +WHERE title LIKE '%计算机%' AND status = 'normal'; + +RAISE NOTICE '-- 按书名精确查询 (如果知道全名):'; +SELECT book_id, isbn, title, authors, available_copies +FROM books +WHERE title = '数学之美' AND status = 'normal'; + +-- 按作者查询 (使用 GIN 索引) +RAISE NOTICE '-- 按作者查询包含 "吴军":'; +SELECT book_id, isbn, title, authors, available_copies +FROM books +WHERE authors @> ARRAY['吴军'] AND status = 'normal'; -- '@>' 表示数组包含 + +-- 按分类号查询 +RAISE NOTICE '-- 按分类号查询 "TP301":'; +SELECT book_id, isbn, title, authors, classification_no, available_copies +FROM books +WHERE classification_no = 'TP301' AND status = 'normal'; + +-- 按ISBN精确查询 +RAISE NOTICE '-- 按ISBN查询 "9787111624927":'; +SELECT book_id, isbn, title, authors, available_copies +FROM books +WHERE isbn = '9787111624927' AND status = 'normal'; + +-- 组合查询:书名包含 "编程" 且 作者包含 "Eric Matthes" +RAISE NOTICE '-- 组合查询:书名包含 "编程" 且 作者包含 "Eric Matthes":'; +SELECT book_id, isbn, title, authors, available_copies +FROM books +WHERE title LIKE '%编程%' AND authors @> ARRAY['Eric Matthes'] AND status = 'normal'; + +-- (2). 查看图书详情及当前可借状态 +RAISE NOTICE '---------- (2) 查看图书详情及当前可借状态 ----------'; +-- 假设查看 ISBN 为 '9787111624927' (深入理解计算机系统) 的图书详情 +-- 首先获取 book_id +DO $$ +DECLARE + v_book_id bigint; +BEGIN + SELECT book_id INTO v_book_id FROM books WHERE isbn = '9787111624927'; + EXECUTE 'SET app.current_book_id = ' || v_book_id; +END $$; + +RAISE NOTICE '-- 查看图书 (ISBN: 9787111624927) 的详情:'; +SELECT + book_id, + isbn, + title, + authors, + publisher, + publish_date, + price, + classification_no, + location, + total_copies, + available_copies, + status, + description, + cover_url, + CASE + WHEN status = 'normal' AND available_copies > 0 THEN '可借阅' + WHEN status = 'normal' AND available_copies = 0 THEN '已全部借出' + ELSE '不可借阅 (' || status::text || ')' + END as borrow_availability +FROM books +WHERE book_id = current_setting('app.current_book_id')::bigint; + +-- (3). 在线预约热门图书(当所有副本都被借出时) +RAISE NOTICE '---------- (3) 在线预约热门图书 ----------'; +-- 假设要预约的书是 'Java核心技术 卷I' (book_id=7),其 available_copies 在测试数据中为0 +-- 首先确认该书确实没有可借副本 +DO $$ +DECLARE + v_target_book_id bigint := 7; -- Java核心技术 卷I + v_available int; +BEGIN + SELECT available_copies INTO v_available FROM books WHERE book_id = v_target_book_id AND status = 'normal'; + IF v_available = 0 THEN + RAISE NOTICE '-- 图书ID % (Java核心技术 卷I) 当前无可用副本,可以进行预约。'; + -- 学生张三 (student_id=1) 进行预约 + -- 检查是否已预约过 + IF NOT EXISTS (SELECT 1 FROM reservations + WHERE book_id = v_target_book_id + AND student_id = current_setting('app.current_student_id')::bigint + AND status IN ('waiting', 'available')) + THEN + INSERT INTO reservations (book_id, student_id, reserve_date, status) + VALUES (v_target_book_id, current_setting('app.current_student_id')::bigint, current_date, 'waiting'); + RAISE NOTICE '-- 学生ID % 已成功预约图书ID %。', current_setting('app.current_student_id')::bigint, v_target_book_id; + ELSE + RAISE NOTICE '-- 学生ID % 已预约过图书ID % 或预约已可取。', current_setting('app.current_student_id')::bigint, v_target_book_id; + END IF; + ELSE + RAISE NOTICE '-- 图书ID % 当前有 % 本可借,无需预约。', v_target_book_id, v_available; + END IF; +END $$; + +-- 查看张三的预约记录来确认 +RAISE NOTICE '-- 查看学生张三的当前预约:'; +SELECT r.reservation_id, b.title, r.reserve_date, r.status +FROM reservations r +JOIN books b ON r.book_id = b.book_id +WHERE r.student_id = current_setting('app.current_student_id')::bigint AND r.status IN ('waiting', 'available'); + + +-- (4). 查看个人借阅历史、当前借阅情况和预约状态 +RAISE NOTICE '---------- (4) 查看个人借阅历史、当前借阅情况和预约状态 ----------'; + +RAISE NOTICE '-- 学生张三 (ID: %) 的当前借阅情况:'; +SELECT br.borrow_id, b.title, b.isbn, br.borrow_date, br.due_date, br.renew_times, br.status +FROM borrow_records br +JOIN books b ON br.book_id = b.book_id +WHERE br.student_id = current_setting('app.current_student_id')::bigint + AND br.status IN ('borrowed', 'overdue'); + +RAISE NOTICE '-- 学生张三 (ID: %) 的借阅历史 (已还):'; +SELECT br.borrow_id, b.title, b.isbn, br.borrow_date, br.return_date, br.status +FROM borrow_records br +JOIN books b ON br.book_id = b.book_id +WHERE br.student_id = current_setting('app.current_student_id')::bigint + AND br.status = 'returned'; + +RAISE NOTICE '-- 学生张三 (ID: %) 的当前预约状态:'; -- 重复上面的预约查询,为了完整性 +SELECT r.reservation_id, b.title, r.reserve_date, r.status +FROM reservations r +JOIN books b ON r.book_id = b.book_id +WHERE r.student_id = current_setting('app.current_student_id')::bigint AND r.status IN ('waiting', 'available'); + +-- (5). 在线续借图书(有限制条件) +RAISE NOTICE '---------- (5) 在线续借图书 ----------'; +-- 假设张三要续借他借阅的《深入理解计算机系统》(book_id=1) +-- borrow_id 需要从他的当前借阅中找到,假设其 borrow_id 为 1 (根据你的测试数据) +DO $$ +DECLARE + v_borrow_id_to_renew bigint; + v_student_id bigint := current_setting('app.current_student_id')::bigint; + v_book_id_of_borrow bigint; + v_max_renew_times int; + v_borrow_duration_days int; + v_current_renew_times int; + v_current_due_date date; + v_current_status borrow_status_enum; + v_is_reserved boolean; +BEGIN + -- 找到张三正在借阅的《深入理解计算机系统》的 borrow_id + SELECT br.borrow_id, br.book_id, br.renew_times, br.due_date, br.status + INTO v_borrow_id_to_renew, v_book_id_of_borrow, v_current_renew_times, v_current_due_date, v_current_status + FROM borrow_records br + JOIN books b ON br.book_id = b.book_id + WHERE br.student_id = v_student_id + AND b.isbn = '9787111624927' -- 《深入理解计算机系统》 + AND br.status = 'borrowed' + LIMIT 1; + + IF NOT FOUND THEN + RAISE NOTICE '-- 未找到学生ID % 对图书ISBN 9787111624927 的可续借记录。'; + RETURN; + END IF; + + RAISE NOTICE '-- 尝试为学生ID % 续借 borrow_id % (Book ID: %)', v_student_id, v_borrow_id_to_renew, v_book_id_of_borrow; + + -- 获取系统参数 + SELECT setting_value::int INTO v_max_renew_times FROM system_settings WHERE setting_key = 'max_renew_times'; + SELECT setting_value::int INTO v_borrow_duration_days FROM system_settings WHERE setting_key = 'borrow_duration_days'; + + -- 检查条件: + -- 1. 状态为 'borrowed' (不能是 'overdue' 或 'lost') + IF v_current_status != 'borrowed' THEN + RAISE NOTICE '-- 续借失败: 图书状态为 %,不是 "borrowed"。', v_current_status; + RETURN; + END IF; + + -- 2. 续借次数未达上限 + IF v_current_renew_times >= v_max_renew_times THEN + RAISE NOTICE '-- 续借失败: 已达到最大续借次数 (%)。', v_max_renew_times; + RETURN; + END IF; + + -- 3. 该书没有其他人预约 + SELECT EXISTS (SELECT 1 FROM reservations WHERE book_id = v_book_id_of_borrow AND status = 'waiting') + INTO v_is_reserved; + IF v_is_reserved THEN + RAISE NOTICE '-- 续借失败: 该图书已被其他用户预约。'; + RETURN; + END IF; + + -- 执行续借 + UPDATE borrow_records + SET due_date = v_current_due_date + (v_borrow_duration_days || ' days')::interval, -- 从原应还日期开始计算 + renew_times = renew_times + 1 + WHERE borrow_id = v_borrow_id_to_renew; + + RAISE NOTICE '-- 续借成功! borrow_id: %, 新的应还日期: %, 当前续借次数: %', + v_borrow_id_to_renew, + (SELECT due_date FROM borrow_records WHERE borrow_id = v_borrow_id_to_renew), + (SELECT renew_times FROM borrow_records WHERE borrow_id = v_borrow_id_to_renew); + +EXCEPTION + WHEN NO_DATA_FOUND THEN + RAISE NOTICE '-- 未找到相关借阅记录或系统参数。'; + WHEN OTHERS THEN + RAISE NOTICE '续借操作发生错误: %', SQLERRM; +END $$; + +-- (6). 查看和缴纳罚款 +RAISE NOTICE '---------- (6) 查看和缴纳罚款 ----------'; +-- 假设学生王五 (S2022003, student_id=3) 有未缴罚款 +DO $$ +DECLARE + v_student_id_wangwu bigint; + v_fine_id_to_pay bigint; + v_amount_to_pay numeric; +BEGIN + SELECT student_id INTO v_student_id_wangwu FROM students WHERE stu_no = 'S2022003'; + IF NOT FOUND THEN + RAISE NOTICE '学生 S2022003 未找到。'; + RETURN; + END IF; + + RAISE NOTICE '-- 查看学生王五 (ID: %) 的未缴罚款:'; + SELECT fine_id, amount, reason, status, issue_date + FROM fines + WHERE student_id = v_student_id_wangwu AND status = 'unpaid'; + + -- 假设王五要缴纳 fine_id 为 (从上面查询结果中选取一个,例如第一个未缴罚款) + SELECT fine_id, amount INTO v_fine_id_to_pay, v_amount_to_pay + FROM fines + WHERE student_id = v_student_id_wangwu AND status = 'unpaid' + ORDER BY issue_date + LIMIT 1; + + IF v_fine_id_to_pay IS NOT NULL THEN + RAISE NOTICE '-- 模拟缴纳学生王五 (ID: %) 的罚款 fine_id: %, 金额: %', v_student_id_wangwu, v_fine_id_to_pay, v_amount_to_pay; + UPDATE fines + SET status = 'paid' + WHERE fine_id = v_fine_id_to_pay; + -- 触发器 trg_freeze_account 会在此 UPDATE 后执行,可能解冻账户 + + RAISE NOTICE '-- 缴纳后,再次查看学生王五 (ID: %) 的未缴罚款:'; + SELECT fine_id, amount, reason, status, issue_date + FROM fines + WHERE student_id = v_student_id_wangwu AND status = 'unpaid'; + + RAISE NOTICE '-- 查看学生王五 (ID: %) 的账户状态 (可能已因罚款缴纳而解冻):'; + SELECT stu_no, name, account_status FROM students WHERE student_id = v_student_id_wangwu; + ELSE + RAISE NOTICE '-- 学生王五 (ID: %) 没有未缴罚款可供缴纳。'; + END IF; +END $$; + +-- (7). 对已借阅图书进行评分和评论 +RAISE NOTICE '---------- (7) 对已借阅图书进行评分和评论 ----------'; +-- 假设张三 (student_id=1) 要对他借阅过的《深入理解计算机系统》(book_id=1)进行评价 +-- (假设他已经借过或者正在借阅) +DO $$ +DECLARE + v_student_id bigint := current_setting('app.current_student_id')::bigint; + v_book_id_to_review bigint; +BEGIN + SELECT book_id INTO v_book_id_to_review FROM books WHERE isbn = '9787111624927'; -- 深入理解计算机系统 + + -- 检查是否已经评论过 (因为有 UNIQUE(book_id, student_id) 约束) + IF EXISTS (SELECT 1 FROM reviews WHERE student_id = v_student_id AND book_id = v_book_id_to_review) THEN + RAISE NOTICE '-- 学生ID % 已评价过图书ID %。'; + UPDATE reviews + SET rating = 5, content = '更新评价:太经典了,常看常新!', review_time = now() + WHERE student_id = v_student_id AND book_id = v_book_id_to_review; + RAISE NOTICE '-- 已更新学生ID % 对图书ID % 的评价。'; + ELSE + -- 应用层面通常会检查该学生是否实际借阅过这本书,这里我们直接插入 + INSERT INTO reviews (book_id, student_id, rating, content, review_time) + VALUES (v_book_id_to_review, v_student_id, 5, '这本书写得太好了,深入浅出,对我帮助很大!', now()); + RAISE NOTICE '-- 学生ID % 已成功评价图书ID %。'; + END IF; + + RAISE NOTICE '-- 查看图书ID % 的所有评价:'; + SELECT s.name as student_name, r.rating, r.content, r.review_time + FROM reviews r + JOIN students s ON r.student_id = s.student_id + WHERE r.book_id = v_book_id_to_review + ORDER BY r.review_time DESC; +END $$; + +-- (8). 查看图书推荐(如基于热门被借阅图书) +RAISE NOTICE '---------- (8) 查看图书推荐(热门图书) ----------'; +RAISE NOTICE '-- 查看当前热门图书 (基于 v_hot_books 视图):'; +SELECT b.isbn, b.title, b.authors, vhb.borrow_cnt +FROM v_hot_books vhb +JOIN books b ON vhb.book_id = b.book_id +ORDER BY vhb.borrow_cnt DESC; + +-- 另一种推荐思路:基于用户所在院系的热门图书 +RAISE NOTICE '-- 查看学生张三所在院系的热门图书 (示例):'; +WITH student_department AS ( + SELECT department FROM students WHERE student_id = current_setting('app.current_student_id')::bigint +), +department_borrows AS ( + SELECT + br.book_id, + COUNT(*) as dept_borrow_count + FROM borrow_records br + JOIN students s ON br.student_id = s.student_id + WHERE s.department = (SELECT department FROM student_department) + GROUP BY br.book_id +) +SELECT b.title, b.authors, db.dept_borrow_count +FROM department_borrows db +JOIN books b ON db.book_id = b.book_id +ORDER BY db.dept_borrow_count DESC +LIMIT 10; + +RAISE NOTICE '---------- 学生功能SQL示例演示完毕 ----------'; \ No newline at end of file diff --git a/src/app/admin/books/[id]/edit/page.tsx b/src/app/admin/books/[id]/edit/page.tsx new file mode 100644 index 0000000..564a206 --- /dev/null +++ b/src/app/admin/books/[id]/edit/page.tsx @@ -0,0 +1,360 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import { Book } from '@/lib/types'; +import { ArrowLeft, Save } from 'lucide-react'; + +export default function EditBookPage() { + const params = useParams(); + const router = useRouter(); + const bookId = params.id as string; + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + + const [formData, setFormData] = useState({ + isbn: '', + title: '', + authors: '', + publisher: '', + publishDate: '', + price: '', + classificationNo: '', + location: '', + totalCopies: '1', + availableCopies: '1', + status: 'normal', + description: '', + coverUrl: '', + }); + + useEffect(() => { + const fetchBook = async () => { + try { + const response = await fetch(`/api/books/${bookId}`); + if (!response.ok) { + throw new Error('Failed to fetch book'); + } + const book: Book = await response.json(); + + setFormData({ + isbn: book.isbn, + title: book.title, + authors: book.authors.join(', '), + publisher: book.publisher || '', + publishDate: book.publishDate || '', + price: book.price || '', + classificationNo: book.classificationNo || '', + location: book.location || '', + totalCopies: book.totalCopies.toString(), + availableCopies: book.availableCopies.toString(), + status: book.status, + description: book.description || '', + coverUrl: book.coverUrl || '', + }); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } finally { + setLoading(false); + } + }; + + if (bookId) { + fetchBook(); + } + }, [bookId]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setSaving(true); + + try { + const updateData = { + ...formData, + authors: formData.authors.split(',').map(author => author.trim()).filter(Boolean), + price: formData.price ? parseFloat(formData.price) : null, + totalCopies: parseInt(formData.totalCopies), + availableCopies: parseInt(formData.availableCopies), + publishDate: formData.publishDate || null, + publisher: formData.publisher || null, + classificationNo: formData.classificationNo || null, + location: formData.location || null, + description: formData.description || null, + coverUrl: formData.coverUrl || null, + }; + + const response = await fetch(`/api/books/${bookId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(updateData), + }); + + if (response.ok) { + router.push(`/admin/books/${bookId}`); + } else { + const errorData = await response.json(); + alert(`更新失败: ${errorData.error || 'Unknown error'}`); + } + } catch (error) { + console.error('Update error:', error); + alert('更新失败'); + } finally { + setSaving(false); + } + }; + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + })); + }; + + if (loading) { + return ( +
+
加载中...
+
+ ); + } + + if (error) { + return ( +
+
错误: {error}
+
+ ); + } + + return ( +
+
+ {/* Header */} +
+
+ +

编辑图书

+
+
+ + {/* Form */} +
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ +