# 1. 设计任务 在数字化时代,图书资源的管理变得越来越重要,图书馆作为知识与信息的集散地,其管理效率直接影响到读者的阅读体验和图书资源的利用效率。这就需要一个功能完备、操作便捷的图书管理系统来对图书和会员信息进行有效管理。图书管理系统的设计旨在提高图书馆的服务质量,优化图书资源配置,增强用户体验,提升管理效率。设计图书管理系统可以提供高效优质的管理服务。 图书管理系统需要完成图书和会员管理的一般性管理工作: - 图书信息的录入、增加、修改、删除、查找和顺序输出等功能。 - 每本图书的登记内容包括书号、书名、作者、所在书库、现存量、库存量和借阅者信息。 - 会员管理,包括增加会员,查询会员,删除会员,修改会员基本信息。 - 实现图书的借阅、归还、预定等功能。 - 实现图书借阅期限的超期预警功能。 - 根据借阅记录计算会员的借阅等级,并设置相应的权限。 - 设计实现菜单方式的交互界面,界面友好,可反复操作,提升用户体验。 - 根据系统的实际运行情况,额外增加图书推荐、图书排行等个性化功能,以增强系统的实用性和吸引力。 # 2. 需求分析 本项目旨在开发一个图书管理系统,主要功能包括图书的管理、用户的管理以及借阅和归还图书的操作。系统将使用链表结构来存储和管理图书和用户信息,以便于实现高效的增删改查操作。 ## 2.1 系统使用到的数据 在日常使用中,用户需要查找图书和用户的相关信息,因此系统中每本图书和每个用户的基本信息需要被记录。以下是图书和用户的属性表: ### 图书属性表 | 属性 | 数据类型 | 描述 | | ---------- | -------- | --------------------------------------- | | 编号 | 字符串 | 每本图书唯一的 ID 信息,不重复 | | 书名 | 字符串 | 图书的名称 | | 作者 | 字符串 | 图书的作者 | | 所在图书馆 | 字符串 | 图书存放的图书馆名称 | | 分类 | 枚举 | 图书的分类(如文学、科学、技术等)共8类 | | 描述 | 字符串 | 图书的简要描述 | | 当前库存 | 整数 | 当前可借阅的图书数量 | | 总库存 | 整数 | 图书的总数量 | | 被借阅次数 | 整数 | 图书被借阅的总次数 | | 评分 | 浮点数 | 图书的平均评分 | ### 用户属性表 | 属性 | 数据类型 | 描述 | | -------- | -------- | ----------------------------- | | 用户ID | 字符串 | 每个用户唯一的ID 信息,不重复 | | 姓名 | 字符串 | 用户的姓名 | | 手机号 | 字符串 | 用户的联系电话 | | 密码 | 字符串 | 用户的登录密码 | | 用户类型 | 枚举 | 用户的类型 (管理员/普通用户) | | 信用分数 | 整数 | 用户的信用评分 | | 借阅记录 | 链表 | 用户的借阅记录 | ## 2.2 系统用到的函数 系统的主要功能包括图书与账号的添加、删除、查找、更新等操作,因此需要设计相应的函数模块以实现这些功能。系统所有功能函数如下表所示: ### 图书管理相关功能函数表 | 函数名 | 功能 | | -------------------- | ---------------------- | | addBook | 添加新图书 | | removeBook | 删除图书 | | findBook | 查找图书 | | updateBooklnfo | 更新图书信息 | | displayAllBooks | 显示所有图书 | | borrowBook | 借阅图书 | | returnBook | 归还图书 | | searchBooks | 根据关键字搜索图书 | | getTopRatedBooks | 获取评分最高的图书 | | getMostBorrowedBooks | 获取借阅次数最多的图书 | ### 用户管理相关功能函数表 | 函数名 | 功能 | | ----------------- | ---------------- | | addMember | 添加新用户 | | removeMember | 删除用户 | | findMember | 查找用户 | | updateUserlnfo | 更新用户信息 | | authenticate | 验证用户身份 | | viewBorrowHistory | 查看用户借阅历史 | | modifyUserlnfo | 修改用户信息 | | deleteUser | 删除用户 | | banUser | 封禁用户 | ## 2.3 抽象数据类型定义 ### ADT BookList ``` ADT BookList { 数据对象:D = {ai | ai ∈ ElemSet, i = 1, 2, …, n, n >= 0} 数据关系:R1 { | ai-1, ai ∈ D, i = 2, …, n} 基本操作: BookList CreateList(void); 初始条件:创建一个空的图书链表; 操作结果:返回一个空的图书链表。 void InsertBook(BookList &list, Book *book); 初始条件:链表 list 已存在,book 为待插入的图书; 操作结果:将图书 book 插入到链表 list 中。 void DeleteBook(BookList &list, const std::string &bookId); 初始条件:链表 list 已存在,bookId 为待删除图书的 ID; 操作结果:从链表 list 中删除 ID 为 bookId 的图书。 Book* FindBook(BookList &list, const std::string &bookId); 初始条件:链表 list 已存在,bookId 为待查找图书的 ID; 操作结果:返回 ID 为 bookId 的图书指针,如果未找到则返回 nullptr。 void UpdateBookInfo(BookList &list, const std::string &bookId, const std::string &newTitle, const std::string &newAuthor); 初始条件:链表 list 已存在,bookId 为待更新图书的 ID; 操作结果:更新 ID 为 bookId 的图书信息。 void DisplayAllBooks(const BookList &list); 初始条件:链表 list 已存在; 操作结果:输出链表 list 中所有图书的信息。 } ``` ### ADT MemberList ``` ADT MemberList { 数据对象:D = {ai | ai ∈ ElemSet, i = 1, 2, …, n, n >= 0} 数据关系:R1 { | ai-1, ai ∈ D, i = 2, …, n} 基本操作: MemberList CreateList(void); 初始条件:创建一个空的用户链表; 操作结果:返回一个空的用户链表。 void InsertMember(MemberList &list, Member *member); 初始条件:链表 list 已存在,member 为待插入的用户; 操作结果:将用户 member 插入到链表 list 中。 void DeleteMember(MemberList &list, const std::string &memberId); 初始条件:链表 list 已存在,memberId 为待删除用户的 ID; 操作结果:从链表 list 中删除 ID 为 memberId 的用户。 Member* FindMember(MemberList &list, const std::string &memberId); 初始条件:链表 list 已存在,memberId 为待查找用户的 ID; 操作结果:返回 ID 为 memberId 的用户指针,如果未找到则返回 nullptr。 void UpdateMemberInfo(MemberList &list, const std::string &memberId, const std::string &newName, const std::string &newPhone); 初始条件:链表 list 已存在,memberId 为待更新用户的 ID; 操作结果:更新 ID 为 memberId 的用户信息。 void DisplayAllMembers(const MemberList &list); 初始条件:链表 list 已存在; 操作结果:输出链表 list 中所有用户的信息。 } ``` # 3. 系统设计 ## 3.1 系统功能模块 本系统主要由以下几个模块组成 #### 1. **Book 模块** - **描述**: 定义了图书的基本信息和操作。 - **文件**: `Book.h` - **功能**: - 图书的基本属性(如书号、书名、作者、类别等)。 - 提供借阅、归还、评分等功能。 - 维护借阅者列表、借阅日期列表和评分列表。 #### 2. **BookManager 模块** - **描述**: 负责管理所有图书的增删查改操作。 - **文件**: `Book.h` - **功能**: - 添加、删除、查找图书。 - 显示所有图书。 - 处理借阅和归还操作。 - 查找超期图书。 - 保存和加载图书数据到文件。 #### 3. **Member 模块** - **描述**: 定义了会员的基本信息和操作。 - **文件**: `Member.h` - **功能**: - 会员的基本属性(如ID、姓名、手机号、密码等)。 - 提供借阅、归还、评分等功能。 - 维护借阅历史、评分记录和偏好类别。 #### 4. **MemberManager 模块** - **描述**: 负责管理所有会员的增删查改操作。 - **文件**: `Member.h` - **功能**: - 添加、删除、查找会员。 - 显示所有会员。 - 处理会员登录验证。 - 保存和加载会员数据到文件。 #### 5. **LibrarySystem 模块** - **描述**: 整合了图书管理和会员管理的功能,提供系统的主入口。 - **文件**: `LibrarySystem.h` - **功能**: - 包含 `BookManager` 和 `MemberManager` 实例。 - 提供系统登录登出功能。 - 提供管理员和用户的不同功能接口。 - 提供图书推荐、排行榜等功能。 #### 6. **UI 模块** - **描述**: 负责与用户交互,显示菜单并处理用户输入 - **文件**: `UI.h` - **功能**: - 显示主菜单、登录界面、注册界面等。 - 处理用户选择的操作(如添加图书、借阅图书等)。 - 显示图书搜索结果、借阅历史等信息。 ### 额外设计 • **数据持久化**:系统需要将图书和用户信息持久化到文件中,以便下次启动时能够加载。 • **错误处理**:系统需要对用户输入进行有效性检查,并在出现错误时给出友好的提示。 • **性能要求**:为了能在合理的时间内完成图书和用户的增删改查操作,确保用户体验,再本地文件查找时增加了字符串哈希和二分查找两个查找方法,当数据量增大时能够确保效率。 ### 模块之间的关系 1. **Book 和 BookManager**: - `BookManager` 使用 `Book` 类来创建和管理图书对象。 - - `BookManager` 中的 `books` 成员变量是一个 `LinkList`,用于存储所有图书。 2. **Member 和 MemberManager**: - - `MemberManager` 使用 `Member` 类来创建和管理会员对象。 - - `MemberManager` 中的 `members` 成员变量是一个 `LinkList`,用于存储所有会员。 3. **LibrarySystem 和 BookManager/MemberManager**: - `LibrarySystem` 包含 `BookManager` 和 `MemberManager` 的实例,作为系统的主控类。 - `LibrarySystem` 提供了对这两个管理器的封装,提供了更高层次的功能接口。 4. **UI 和 LibrarySystem**: - `UI` 类依赖于 `LibrarySystem` 来执行具体的操作。 - `UI` 负责显示菜单并根据用户输入调用 `LibrarySystem` 中的方法。 ## 3.2 设计数据元素 ### 书籍信息 ```cpp struct Book { std::string bookId; // 书号 std::string title; // 书名 std::string author; // 作者 std::string library; // 所在书库 std::string description; // 图书简介 BookCategory category; // 图书类别 int currentStock; // 现存量 int totalStock; // 库存量 LinkList borrowerPhones; // 借阅者手机号列表 LinkList borrowDates; // 借阅日期列表 LinkList ratings; // 用户评分列表 double averageRating; // 平均评分 }; ``` ### 用户信息 ```cpp struct Member { std::string id; // 用户ID std::string name; // 用户名 std::string phone; // 手机号 std::string password; // 密码 MemberType type; // 用户类型 MemberLevel level; // 会员等级 int creditScore; // 信用分数 // 借阅相关 LinkList borrowedBooks; // 当前借阅的书籍ID列表 LinkList borrowDates; // 对应的借阅日期 LinkList borrowHistory; // 历史借阅记录 LinkList bookRatings; // 用户对书籍的评分记录 // 用户偏好 LinkList preferredCategories; // 偏好的图书类别 double categoryWeights[8]; // 各类别的权重(对应BookCategory枚举) // 预约相关 LinkList reservedBooks; // 预约图书列表 std::vector borrowRecords; // 当前借阅记录 }; ``` ## 3.3 各个函数的设计 ### 插入函数 ```cpp bool LibrarySystem::addBook(const std::string &id, const std::string &title, const std::string &author, const std::string &library, int stock); ``` **设计思路与算法描述**: 1. 检查是否已存在相同ID的图书。 2. 创建新的`Book`对象。 3. 将新图书添加到`bookManager`中。 4. 保存图书数据到文件。 ![](pics/flowChart/单链表尾插.png) 插入用户同理 ### 删除函数 ```cpp bool LibrarySystem::removeBook(const std::string &bookId); ``` **设计思路与算法描述**: 1. 检查是否有权限删除。 2. 读取文件,跳过要删除的记录。 3. 替换原文件。 4. 更新数据结构。 删除用户同理 ### 查找函数 ```cpp Book *LibrarySystem::findBook(const std::string &bookId) const; ``` **设计思路与算法描述**: 1. 遍历图书或会员列表。 2. 根据ID查找对应的图书或会员。 3. 返回找到的对象。 查找用户同理 ### 修改函数 ```cpp bool LibrarySystem::updateBookInfo(const std::string &bookId, const std::string &title, const std::string &author, const std::string &library); ``` **设计思路与算法描述**: 1. 查找要修改的图书或会员。 2. 更新相关信息。 3. 保存修改后的数据到文件。 修改用户同理 ### 排序函数 ```cpp LinkList LibrarySystem::getTopRatedBooks(int limit) const; LinkList LibrarySystem::getMostBorrowedBooks(int limit) const; ``` **设计思路与算法描述**: 1. 收集所有图书的评分或借阅次数。 2. 使用选择排序对图书进行排序。 3. 返回前`limit`个图书ID。 ![](pics/flowChart/选择排序.png) # 4. 编码实现 ## 4.1 LinkList ### 4.1.1 建立模块设计 ```cpp LinkList* CreateList(void) { LinkList *head = new LinkList(); // 创建头结点 ListNode *p, *rear; int flag = 0; // 结束指标置 0 rear = head; // 尾指针初始化指向头结点 while (flag == 0) { p = new ListNode(); // 申请新结点 std::cout << "请输入图书ID: "; std::cin >> p->data; rear->next = p; // 新结点连接到尾结点之后 rear = p; // 尾指针指向新结点 std::cout << "结束建表吗? (1/0): "; std::cin >> flag; // 读入一个标志数据 } rear->next = nullptr; // 终端结点指针域置空 return head; // 返回链表头指针 } ``` **该模块的时间复杂度是 O(n)**,其中 n 是链表中节点的数量。**空间复杂度为 O(n)**,因为每个节点都需要分配内存。 ### 4.1.2 插入模块设计 ```cpp void InsertNode(LinkList *head, ListNode *p) { ListNode *p1, *p2; p1 = head; p2 = p1->next; while (p2 != nullptr && p2->data < p->data) { p1 = p2; // p1 指向刚访问过的结点 p2 = p2->next; // p2 指向表的下一个结点 } p1->next = p; // 插入 p 所指向的结点 p->next = p2; // 连接表中剩余部分 head->len++; // 更新链表长度 } ``` **该模块的时间复杂度是 O(n)**,其中 n 是链表中节点的数量。**空间复杂度为 O(1)**,因为只使用了常量级别的额外空间。 ### 4.1.3 查询模块设计 ```cpp ListNode* ListFind(LinkList *head, int value) { ListNode *p = head->head->next; while (p != nullptr && p->data < value) { p = p->next; } if (p == nullptr || p->data != value) { p = nullptr; // 没有查到要查找的节点 } return p; } ``` **该模块的时间复杂度是 O(n)**,其中 n 是链表中节点的数量。**空间复杂度为 O(1)**,因为只使用了常量级别的额外空间。 ### 4.1.4 删除模块设计 ```cpp void DelNode(LinkList *head, ListNode *p) { ListNode *q = head->head; while (q->next != nullptr && q->next != p) { q = q->next; } if (q->next == p) { q->next = p->next; // 删除结点 delete p; // 释放被删除的结点空间 head->len--; // 更新链表长度 std::cout << "节点已被删除!\n"; } else { std::cout << "节点没有被删除!\n"; } } ``` **该模块的时间复杂度是 O(n)**,其中 n 是链表中节点的数量。**空间复杂度为 O(1)**,因为只使用了常量级别的额外空间。 ### 4.1.5 排序模块设计 ```cpp void SortList(LinkList *head) { head->sortForLinkList(); // 使用快速排序对链表进行排序 } ``` **该模块的时间复杂度是 O(n log n)**,其中 n 是链表中节点的数量。**空间复杂度为 O(log n)**,因为快速排序的递归调用栈深度为 O(log n)。 ### 4.1.6 显示模块设计 ```cpp void DisplayList(LinkList *head) { head->displayAllNodes(); // 打印所有节点 } ``` **该模块的时间复杂度是 O(n)**,其中 n 是链表中节点的数量。**空间复杂度为 O(1)**,因为只使用了常量级别的额外空间。 ## 4.2 Book ### 4.2.1 构造函数 ```cpp Book::Book(const std::string &id, const std::string &title, const std::string &author, const std::string &library, BookCategory category, const std::string &desc, int stock) : bookId(id), title(title), author(author), library(library), // 初始化基本信息 category(category), description(desc), currentStock(stock), // 初始化分类和库存 totalStock(stock), averageRating(0) // 初始化总库存和评分 { // 构造函数实现 } ``` **设计思路与算法描述**: - 初始化图书的基本信息,包括 ID、标题、作者、所在图书馆、分类、描述、库存等。 - 设置当前库存和总库存为相同值。 - 初始化平均评分为 0。 **时间复杂度**:O(1)。 **空间复杂度**:O(1)。 ### 4.2.2 借阅处理 ```cpp bool Book::borrow(const std::string &userPhone) { if (currentStock <= 0) // 检查是否有库存 return false; for (int i = 1; i <= borrowerPhones.length(); i++) // 遍历借阅者列表 { if (std::to_string(borrowerPhones.getPointer(i)->data) == userPhone) // 检查是否重复借阅 return false; } borrowerPhones.addNodeToEnd(std::stoi(userPhone)); // 添加借阅者 borrowDates.addNodeToEnd(std::time(nullptr)); // 记录借阅时间 currentStock--; // 减少库存 return true; } ``` **设计思路与算法描述**: 1. 检查当前库存是否为 0,如果是则返回 `false`。 2. 遍历借阅者列表,检查是否已借阅该图书,如果是则返回 `false`。 3. 将借阅者手机号添加到借阅者列表。 4. 记录借阅时间。 5. 减少当前库存。 6. 返回 `true`。 **时间复杂度**:O(n),其中 n 是借阅者列表的长度。 **空间复杂度**:O(1)。 ### 4.2.3 归还处理 ```cpp bool Book::returnBook(const std::string &userPhone) { for (int i = 1; i <= borrowerPhones.length(); i++) // 遍历借阅者列表 { if (std::to_string(borrowerPhones.getPointer(i)->data) == userPhone) // 找到借阅记录 { borrowerPhones.deleteNode(i); // 删除借阅者记录 borrowDates.deleteNode(i); // 删除借阅时间记录 currentStock++; // 增加库存 return true; } } return false; } ``` **设计思路与算法描述**: 1. 遍历借阅者列表,查找匹配的借阅记录。 2. 如果找到匹配的借阅记录,删除借阅者记录和借阅时间记录。 3. 增加当前库存。 4. 返回 `true`。 5. 如果未找到匹配的借阅记录,返回 `false`。 **时间复杂度**:O(n),其中 n 是借阅者列表的长度。 **空间复杂度**:O(1)。 ### 4.2 .4 检查逾期 ```cpp bool Book::isOverdue(const std::string &userPhone, int maxDays) const { for (int i = 1; i <= borrowerPhones.length(); i++) // 遍历借阅者列表 { if (std::to_string(borrowerPhones.getPointer(i)->data) == userPhone) // 找到借阅记录 { time_t borrowDate = borrowDates.getPointer(i)->data; // 获取借阅时间 time_t now = std::time(nullptr); // 获取当前时间 return (now - borrowDate) > (maxDays * 24 * 3600); // 检查是否超期 } } return false; // 未找到借阅记录 } ``` **设计思路与算法描述**: 1. 遍历借阅者列表,查找匹配的借阅记录。 2. 如果找到匹配的借阅记录,获取借阅时间和当前时间,计算时间差。 3. 如果时间差大于最大借阅天数,则返回 `true`,表示逾期。 4. 如果未找到匹配的借阅记录,返回 `false`。 **时间复杂度**:O(n),其中 n 是借阅者列表的长度。 **空间复杂度**:O(1)。 ### 4.2.5 检查可借阅 ```cpp bool Book::isAvailable() const { return currentStock > 0; } ``` **设计思路与算法描述**: - 检查当前库存是否大于 0。 - 如果大于 0,返回 `true`,表示可借阅。 - 否则,返回 `false`。 **时间复杂度**:O(1)。 **空间复杂度**:O(1)。 ### 4.2.6 更新图书信息 ```cpp void Book::updateInfo(const std::string &newTitle, const std::string &newAuthor, const std::string &newLibrary, const std::string &newDesc) { title = newTitle; // 更新标题 author = newAuthor; // 更新作者 library = newLibrary; // 更新馆藏地点 description = newDesc; // 更新描述 } ``` **设计思路与算法描述**: - 更新图书的标题、作者、馆藏地点和描述。 - 如果参数为空字符串,则保持原值不变。 **时间复杂度**:O(1)。 **空间复杂度**:O(1)。 ### 4.2.7 更新库存数量 ```cpp void Book::updateStock(int newStock) { if (newStock < 0) // 验证库存合法性 return; int diff = newStock - totalStock; // 计算库存变化 totalStock = newStock; // 更新总库存 currentStock += diff; // 更新当前库存 if (currentStock < 0) // 确保当前库存非负 currentStock = 0; } ``` **设计思路与算法描述**: 1. 验证新库存是否为负数,如果是则返回。 2. 计算库存变化。 3. 更新总库存。 4. 更新当前库存。 5. 确保当前库存非负。 **时间复杂度**:O(1)。 **空间复杂度**:O(1)。 ### 4.2.8 添加评分 ```cpp void Book::addRating(int rating) { if (rating < 1 || rating > 5) // 验证评分范围 return; ratings.addNodeToEnd(rating); // 添加新评分 calculateAverageRating(); // 重新计算平均分 } ``` **设计思路与算法描述**: 1. 验证评分是否在 1 到 5 之间,如果不是则返回。 2. 将新评分添加到评分列表。 3. 重新计算平均评分。 **时间复杂度**:O(n),其中 n 是评分列表的长度。 **空间复杂度**:O(1)。 ### 4.2.9 计算平均评分 ```cpp void Book::calculateAverageRating() { if (ratings.length() == 0) // 检查是否有评分 { averageRating = 0; return; } double sum = 0; for (int i = 1; i <= ratings.length(); i++) // 计算评分总和 { sum += ratings.getPointer(i)->data; } averageRating = sum / ratings.length(); // 计算平均分 } ``` **设计思路与算法描述**: 1. 检查评分列表是否为空,如果是则将平均评分设为 0 并返回。 2. 计算评分列表中所有评分的总和。 3. 计算平均评分并更新 `averageRating`。 **时间复杂度**:O(n),其中 n 是评分列表的长度。 **空间复杂度**:O(1)。 #### 3.10 BookManager 构造函数 ```cpp BookManager::BookManager(const std::string &filename) : dataFile(filename) { loadFromFile(); } ``` **设计思路与算法描述**: - 初始化数据文件路径。 - 从文件加载图书数据。 **时间复杂度**:O(n),其中 n 是文件中的图书数量。 **空间复杂度**:O(n)。 ## 4.3 BookMamager ### 4.3.1 添加图书 ```cpp bool BookManager::addBook(Book *book) { if (!findBook(book->getBookId())) { books.addNodeToEnd(reinterpret_cast(book)); return true; } return false; } ``` **设计思路与算法描述**: 1. 检查图书是否已存在,如果存在则返回 `false`。 2. 将图书添加到图书列表。 3. 返回 `true`。 **时间复杂度**:O(n),其中 n 是图书列表的长度。 **空间复杂度**:O(1)。 ### 4.3.2 查找图书 ```cpp Book *BookManager::findBook(const std::string &bookId) const { for (int i = 1; i <= books.length(); i++) { Book *book = reinterpret_cast(books.getPointer(i)->data); if (book->getBookId() == bookId) { return book; } } return nullptr; } ``` **设计思路与算法描述**: 1. 遍历图书列表,查找匹配的图书。 2. 如果找到匹配的图书,返回图书指针。 3. 如果未找到匹配的图书,返回 `nullptr`。 **时间复杂度**:O(n),其中 n 是图书列表的长度。 **空间复杂度**:O(1)。 ### 4.3.3 显示所有图书 ```cpp void BookManager::displayAllBooks() const { for (int i = 1; i <= books.length(); i++) { Book *book = reinterpret_cast(books.getPointer(i)->data); std::cout << "书号: " << book->getBookId() << ", 书名: " << book->getTitle() << ", 作者: " << book->getAuthor() << ", 馆藏地: " << book->getLibrary() << ", 在库数量: " << book->getCurrentStock() << "/" << book->getTotalStock() << std::endl; } } ``` **设计思路与算法描述**: - 遍历图书列表,打印每本书的基本信息。 **时间复杂度**:O(n),其中 n 是图书列表的长度。 **空间复杂度**:O(1)。 ### 4.3.4 保存到文件 ```cpp bool BookManager::saveToFile() const { std::ofstream file(dataFile, std::ios::trunc); // 使用 trunc 模式打开文件 if (!file.is_open()) return false; for (int i = 1; i <= books.length(); i++) { Book *book = reinterpret_cast(books.getPointer(i)->data); file << book->getBookId() << "," << book->getTitle() << "," << book->getAuthor() << "," << book->getLibrary() << "," << book->getDescription() << "," << book->getTotalStock() << "," << book->getCurrentStock() << "," << book->getBorrowCount() << "," << book->getAverageRating() << "," << book->getRatingCount() << "\n"; } return true; } ``` **设计思路与算法描述**: 1. 使用 `trunc` 模式打开文件,清空原有内容。 2. 遍历图书列表,将每本书的信息按 CSV 格式写入文件。 3. 返回 `true` 表示保存成功。 **时间复杂度**:O(n),其中 n 是图书列表的长度。 **空间复杂度**:O(1)。 ### 4.3.5 从文件加载 ```cpp bool BookManager::loadFromFile() { std::ifstream file(dataFile); if (!file.is_open()) return false; std::string line; while (std::getline(file, line)) { std::stringstream ss(line); std::string id, title, author, library, desc; std::string totalStock, currentStock, borrowCount, avgRating, ratingCount; std::getline(ss, id, ','); std::getline(ss, title, ','); std::getline(ss, author, ','); std::getline(ss, library, ','); std::getline(ss, desc, ','); std::getline(ss, totalStock, ','); std::getline(ss, currentStock, ','); std::getline(ss, borrowCount, ','); std::getline(ss, avgRating, ','); std::getline(ss, ratingCount); Book *book = new Book(id, title, author, library, BookCategory::OTHER, desc, std::stoi(totalStock)); book->updateStock(std::stoi(currentStock)); books.addNodeToEnd(reinterpret_cast(book)); } return true; } ``` **设计思路与算法描述**: 1. 打开数据文件,读取每行数据。 2. 使用 `stringstream` 解析每行数据,提取图书信息。 3. 创建 `Book` 对象并添加到图书列表。 4. 返回 `true` 表示加载成功。 **时间复杂度**:O(n),其中 n 是文件中的图书数量。 **空间复杂度**:O(n)。 ### 4.3.6 删除图书 ```cpp bool BookManager::removeBook(const std::string &bookId) { for (int i = 1; i <= books.length(); i++) { Book *book = reinterpret_cast(books.getPointer(i)->data); if (book->getBookId() == bookId) { delete book; books.deleteNode(i); return true; } } return false; } ``` **设计思路与算法描述**: 1. 遍历图书列表,查找匹配的图书。 2. 如果找到匹配的图书,删除图书对象并从图书列表中删除节点。 3. 返回 `true` 表示删除成功。 4. 如果未找到匹配的图书,返回 `false`。 **时间复杂度**:O(n),其中 n 是图书列表的长度。 **空间复杂度**:O(1)。 ### 4.3.7 搜索图书 ```cpp LinkList BookManager::searchBooks(const std::string &keyword) const { LinkList results; for (int i = 1; i <= books.length(); i++) { Book *book = reinterpret_cast(books.getPointer(i)->data); if (book->getTitle().find(keyword) != std::string::npos || book->getAuthor().find(keyword) != std::string::npos) { results.addNodeToEnd(reinterpret_cast(book)); } } return results; } ``` **设计思路与算法描述**: 1. 遍历图书列表,查找书名或作者名包含关键字的图书。 2. 将匹配的图书添加到结果链表。 3. 返回结果链表。 **时间复杂度**:O(n * m),其中 n 是图书列表的长度,m 是关键字的长度。 **空间复杂度**:O(k),其中 k 是匹配的图书数量。 ### 4.3.8 处理借阅请求 ```cpp bool BookManager::borrowBook(const std::string &bookId, const std::string &userPhone) { Book *book = findBook(bookId); if (book && book->borrow(userPhone)) { saveToFile(); // 确保保存时只更新 currentStock return true; } return false; } ``` **设计思路与算法描述**: 1. 查找图书。 2. 如果找到图书且借阅成功,保存图书数据到文件。 3. 返回 `true` 表示借阅成功。 4. 否则,返回 `false`。 **时间复杂度**:O(n),其中 n 是图书列表的长度。 **空间复杂度**:O(1)。 ### 4.3.9 处理归还请求 ```cpp bool BookManager::returnBook(const std::string &bookId, const std::string &userPhone) { Book *book = findBook(bookId); if (book && book->returnBook(userPhone)) { saveToFile(); // 确保保存时只更新 currentStock return true; } return false; } ``` **设计思路与算法描述**: 1. 查找图书。 2. 如果找到图书且归还成功,保存图书数据到文件。 3. 返回 `true` 表示归还成功。 4. 否则 ## 4.4 Member 与`Book`高度类似 ### 4.4.1 借阅图书 ```cpp bool Member::borrowBook(const std::string &bookId) { if (!canBorrowMore()) { return false; } // 检查是否已借阅该书 for (int i = 1; i <= borrowedBooks.length(); i++) { if (std::to_string(borrowedBooks.getPointer(i)->data) == bookId) { return false; } } borrowedBooks.addNodeToEnd(std::stoi(bookId)); borrowDates.addNodeToEnd(std::time(nullptr)); addBorrowRecord(bookId); // 添加借阅记录 saveBorrowRecords(); // 保存借阅记录 return true; } ``` **设计思路与算法描述**: 1. 检查是否还能继续借书,如果不能则返回 `false`。 2. 检查是否已借阅该书,如果已借阅则返回 `false`。 3. 将图书 ID 添加到借阅列表。 4. 记录借阅时间。 5. 添加借阅记录并保存到文件。 6. 返回 `true` 表示借阅成功。 **时间复杂度**:O(n),其中 n 是借阅列表的长度。 **空间复杂度**:O(1)。 ### 4.4.2 归还图书 ```cpp bool Member::returnBook(const std::string &bookId) { for (int i = 1; i <= borrowedBooks.length(); i++) { if (std::to_string(borrowedBooks.getPointer(i)->data) == bookId) { // 检查是否超期 time_t borrowDate = borrowDates.getPointer(i)->data; time_t now = std::time(nullptr); int maxDays = getMaxBorrowDays(); if ((now - borrowDate) > (maxDays * 24 * 3600)) { // 超期扣分 updateCreditScore(-5); } borrowedBooks.deleteNode(i); borrowDates.deleteNode(i); // 更新借阅记录 for (auto &record : borrowRecords) { if (record.bookId == bookId && record.returnDate == 0) { record.returnDate = std::time(nullptr); saveBorrowRecords(); // 保存更新后的记录 break; } } return true; } } return false; } ``` **设计思路与算法描述**: 1. 遍历借阅列表,查找匹配的图书。 2. 如果找到匹配的图书,检查是否超期,如果超期则扣分。 3. 删除借阅记录和借阅时间记录。 4. 更新借阅记录中的归还时间并保存到文件。 5. 返回 `true` 表示归还成功。 6. 如果未找到匹配的图书,返回 `false`。 **时间复杂度**:O(n),其中 n 是借阅列表的长度。 **空间复杂度**:O(1)。 ### 4.4.3 预约图书 ```cpp bool Member::reserveBook(const std::string &bookId) { // 检查预约数量限制(假设最多预约3本) if (reservedBooks.length() >= 3) { return false; } reservedBooks.addNodeToEnd(std::stoi(bookId)); return true; } ``` **设计思路与算法描述**: 1. 检查预约数量是否达到上限,如果达到则返回 `false`。 2. 将图书 ID 添加到预约列表。 3. 返回 `true` 表示预约成功。 **时间复杂度**:O(1)。 **空间复杂度**:O(1)。 ### 4.4.4获取图书应还日期 ```cpp time_t Member::getDueDate(const std::string &bookId) const { auto it = std::find_if(borrowRecords.begin(), borrowRecords.end(), [&bookId](const BorrowRecord &record) { return record.bookId == bookId; }); return it != borrowRecords.end() ? it->dueDate : 0; } ``` **设计思路与算法描述**: 1. 使用 `std::find_if` 查找匹配的借阅记录。 2. 如果找到匹配的借阅记录,则返回应还日期。 3. 如果未找到匹配的借阅记录,则返回 0。 **时间复杂度**:O(n),其中 n 是借阅记录的长度。 **空间复杂度**:O(1)。 ### 4.4.5 验证密码 ```cpp bool Member::verifyPassword(const std::string &inputPassword) const { return password == inputPassword; } ``` **设计思路与算法描述**: - 检查输入的密码是否与存储的密码相同。 - 如果相同则返回 `true`,否则返回 `false`。 **时间复杂度**:O(1)。 **空间复杂度**:O(1)。 ## 4.6 MemberManager 与`BookManager`高度类似 ### 4.6.1 验证用户身份 ```cpp Member *MemberManager::authenticate(const std::string &id, const std::string &password) { Member *member = findMember(id); if (!member) { return nullptr; } if (member->verifyPassword(password)) { return member; } return nullptr; } ``` **设计思路与算法描述**: 1. 根据 ID 查找会员。 2. 如果找到会员,则验证密码。 3. 如果密码正确,则返回会员指针。 4. 如果密码错误或未找到会员,则返回 `nullptr`。 **时间复杂度**:O(n),其中 n 是会员列表的长度。 **空间复杂度**:O(1)。 ### 4.6.2 使用二分查找来查找会员 ```cpp Member *MemberManager::findMemberBinary(const std::string &id) const { // 创建临时数组存储排序后的会员指针 int length = members.length(); Member **sortedMembers = new Member *[length]; for (int i = 1; i <= length; i++) { sortedMembers[i - 1] = reinterpret_cast(members.getPointer(i)->data); } // 冒泡排序按id排序 for (int i = 0; i < length - 1; i++) { for (int j = 0; j < length - i - 1; j++) { if (sortedMembers[j]->getId() > sortedMembers[j + 1]->getId()) { Member *temp = sortedMembers[j]; sortedMembers[j] = sortedMembers[j + 1]; sortedMembers[j + 1] = temp; } } } // 二分查找 int left = 0; int right = length - 1; Member *result = nullptr; while (left <= right) { int mid = left + (right - left) / 2; std::string midId = sortedMembers[mid]->getId(); if (midId == id) { result = sortedMembers[mid]; break; } if (midId < id) { left = mid + 1; } else { right = mid - 1; } } // 释放临时数组 delete[] sortedMembers; return result; } ``` **设计思路与算法描述**: 1. 创建临时数组存储排序后的会员指针。 2. 使用冒泡排序按 ID 对会员进行排序。 3. 使用二分查找查找匹配的会员。 4. 释放临时数组并返回结果。 **时间复杂度**:O(n^2 + log n),其中 n 是会员列表的长度(冒泡排序的时间复杂度为 O(n^2),二分查找的时间复杂度为 O(log n))。 **空间复杂度**:O(n)。 ### 4.6.3 计算字符串的 BKDR 哈希值 ```cpp unsigned int MemberManager::BKDRHash(const std::string &str) const { unsigned int seed = 131; // 种子数,可选用31,131,1313等质数,质数作为种子可以让哈希值分布更均匀 unsigned int hash = 0; // 初始化哈希值 // 对字符串中的每个字符进行处理 for (char c : str) { // 哈希计算公式: hash = hash * seed + 当前字符 // 这样可以保证字符串中每个位置的字符都对最终哈希值有贡献 hash = hash * seed + c; } return hash; } ``` **设计思路与算法描述**: - 使用 BKDR 哈希算法计算字符串的哈希值。 - 初始化哈希值为 0。 - 遍历字符串中的每个字符,使用公式 `hash = hash * seed + 当前字符` 更新哈希值。 - 返回最终的哈希值。 **时间复杂度**:O(n),其中 n 是字符串的长度。 **空间复杂度**:O(1)。 ### 4.6.4 使用哈希表查找会员 ```cpp Member *MemberManager::findMemberByNameHash(const std::string &name) const { // 定义哈希表大小为1024,这是一个权衡值: // - 太小会增加冲突概率 // - 太大会浪费内存空间 const int TABLE_SIZE = 1024; Member **hashTable[TABLE_SIZE]; // 哈希表,每个位置存储一个动态数组(桶) int hashSizes[TABLE_SIZE] = {0}; // 记录每个桶当前存储的元素数量 int hashCapacities[TABLE_SIZE] = {0}; // 记录每个桶的当前容量 // 初始化哈希表,将所有桶指针设为nullptr for (int i = 0; i < TABLE_SIZE; i++) { hashTable[i] = nullptr; } // 将所有会员插入哈希表 for (int i = 1; i <= members.length(); i++) { Member *member = reinterpret_cast(members.getPointer(i)->data); // 计算哈希值并取模,确保在表大小范围内 unsigned int hash = BKDRHash(member->getName()) % TABLE_SIZE; // 如果当前桶已满或未分配,需要分配或扩展空间 if (hashSizes[hash] >= hashCapacities[hash]) { // 使用倍增策略扩展容量,初始容量为4 int newCapacity = hashCapacities[hash] == 0 ? 4 : hashCapacities[hash] * 2; Member **newBucket = new Member *[newCapacity]; // 将原有数据复制到新空间 for (int j = 0; j < hashSizes[hash]; j++) { newBucket[j] = hashTable[hash][j]; } // 释放旧空间并更新指针和容量 delete[] hashTable[hash]; hashTable[hash] = newBucket; hashCapacities[hash] = newCapacity; } // 将会员指针添加到对应的桶中 hashTable[hash][hashSizes[hash]++] = member; } // 查找目标会员 unsigned int hash = BKDRHash(name) % TABLE_SIZE; // 在对应的桶中线性查找 for (int i = 0; i < hashSizes[hash]; i++) { if (hashTable[hash][i]->getName() == name) { // 找到目标会员后,清理所有动态分配的内存 for (int i = 0; i < TABLE_SIZE; i++) { delete[] hashTable[i]; } return hashTable[hash][i]; } } // 未找到目标会员,清理所有动态分配的内存 for (int i = 0; i < TABLE_SIZE; i++) { delete[] hashTable[i]; } return nullptr; } ``` **设计思路与算法描述**: 1. 定义哈希表大小为 1024。 2. 初始化哈希表,将所有桶指针设为 `nullptr`。 3. 遍历会员列表,将每个会员插入哈希表。 4. 计算目标会员的哈希值并查找对应的桶。 5. 在对应的桶中线性查找目标会员。 6. 如果找到目标会员,则清理所有动态分配的内存并返回会员指针。 7. 如果未找到目标会员,则清理所有动态分配的内存并返回 `nullptr`。 **时间复杂度**: - 平均情况:O(1) - 最坏情况:O(n)(当所有元素都哈希到同一个桶时) **空间复杂度**:O(n),其中 n 是会员列表的长度。 # 5. 调试分析与运行结果 ## 5.1 调试分析 在程序调试过程中,遇到了 **C++字符串输入添加图书导致空指针异常** 的问题 * **问题描述**: 在添加图书时,用户输入的字符串(如书名、作者等)未能正确初始化,导致后续操作中出现空指针异常。例如,在调用 `Book` 类的构造函数时,传入的字符串为空或未正确赋值。 * **问题原因**: 主要原因是用户输入的字符串没有正确传递给 `Book` 对象的构造函数,或者在输入过程中出现了截断或丢失的情况。此外,可能存在未检查用户输入的有效性,直接将空字符串传递给构造函数。 * **解决方法**: 修改了输入处理逻辑,确保用户输入的字符串在传递给构造函数之前进行了有效性检查。具体来说,在 `LibrarySystem::addBook` 方法中增加了对输入字符串的非空检查,并在控制台提示用户重新输入无效数据。最终通过这些修改,确保了输入字符串的正确性和完整性。 ## 5.2 运行结果 ![用户信息修改](pics/screenshots/用户信息修改.png) ![用户信息查询](pics/screenshots/用户信息查询.png) ![管理员用户登录](pics/screenshots/管理员用户登录.png) ![评分排行](pics/screenshots/评分排行.png) ![下架图书](pics/screenshots/下架图书.png) ![主界面](pics/screenshots/主界面.png) ![修改图书](pics/screenshots/修改图书.png) ![借阅历史](pics/screenshots/借阅历史.png) ![借阅图书](pics/screenshots/借阅图书.png) ![借阅次数排行](pics/screenshots/借阅次数排行.png) ![删除用户](pics/screenshots/删除用户.png) ![图书推荐](pics/screenshots/图书推荐.png) ![归还图书](pics/screenshots/归还图书.png) ![查找书籍](pics/screenshots/查找书籍.png) ![注册用户](pics/screenshots/注册用户.png) ![添加图书](pics/screenshots/添加图书.png) # 6. 课设总结 通过这次课程设计,让我学到了很多以前没有接触过的知识,也让我对C++编程和数据结构有了新的了解。想要设计一个完整的系统,首先需要进行需求分析,对题目要求有深入的理解,其次就是功能设计,整个系统可以分为图书管理、会员管理和用户交互三个主要模块来实现,最后就是数据的测试,这样才确保系统的完整性。 系统功能全部完成了,但是由于设计的简单性,导致存在很多的问题。比如,系统的性能在处理大量数据时不够稳定。这里就需要我们进行改善,我们可以利用数据结构优化算法,对系统进行性能调优。 此次课程设计使我对自己的专业有了更深的认识,提高了自己实际动手能力。在编程过程中,可能在实现细节上不到位,对更多的功能也未能实现。我将不断提高自己,尤其在数据结构和算法方面去学习,多看相关的书籍和应用性较强的数据结构系统。总而言之,此次课程设计让我受益颇丰。 另外, 此次课设的系统设计让我对项目的文件结构有了新的认识,不再像往常一样单文件编写,而是构建了清晰的项目文件结构项目文件结构清晰,便于管理和维护。以下是主要的文件结构: ``` build/ src/ STL/ LinkList.cpp Book.cpp LibrarySystem.cpp Member.cpp UI.cpp include/ STL/ LinkList.h Book.h Constants.h LibrarySystem.h Member.h UI.h main.cpp ``` - **`build/`**:包含构建生成的文件,如Makefile、CMake配置文件等。 - **`src/`**:包含源代码文件,如 `Book.cpp`、`LibrarySystem.cpp`、`Member.cpp`、`UI.cpp` 和 `STL/LinkList.cpp`。 - **`include/`**:包含头文件,如 `Book.h`、`Constants.h`、`LibrarySystem.h`、`Member.h`、`UI.h` 和 `STL/LinkList.h`。 - **`main.cpp`**:程序入口文件,负责初始化和启动系统。 除了文件结构之外,我对CMake的使用有了更深入的理解,也掌握了如何使用CMake进行项目构建和管理,使用CMakeLists.txt文件进行项目配置,简化了构建过程,通过CMake配置编译选项,如编译标准、编译器路径等。这不仅提高了我的编程技能,也为我今后的项目开发打下了坚实的基础。 # 7. 谢辞 在即将完成这篇课程设计之际,我怀着感激之情写下这段谢辞。首先,我要向我的指导老师表达最诚挚的感谢。老师不仅教授了我数据结构的基础知识,更为我提供了宝贵的指导和建议,使我能够深入理解图书管理和用户数据存储的复杂性,并构建出清晰、有效的数据结构。特别是在查找算法的学习中,通过应用老师所教的二分查找和哈希查找方法,极大地提高了程序的效率和响应速度。 此外,我也要感谢学校提供的优良学习环境和资源支持,图书馆丰富的藏书以及在线数据库为我的研究工作提供了坚实的基础。实验室开放的时间和设备帮助我在实践中验证理论,进一步加深了我对专业知识的理解。 特别感谢我的室友,在我为了这个项目连续几个夜晚熬夜奋战时,他们不仅没有因为我的晚归而责怪我,反而给予了理解和包容,为我争取了宝贵的时间。他们的支持和鼓励是我克服困难、坚持到最后的动力之一。 同时,我也想感谢那些在我遇到难题时伸出援手的同学和朋友们。无论是在线讨论还是面对面交流,每一次的帮助都让我受益匪浅,也让我感受到团队合作的力量和温暖。 再次感谢所有给予我帮助和支持的人们,你们的努力和付出成就了今天的成果。未来的学习道路上,我会继续秉持这种精神,不断进步,迎接更多的挑战。 # 8. 参考文献 [1] 严蔚敏.数据结构(C语言版).北京.清华大学出版社.2012 [2] Mark Allen Weiss.Data Structures and Algorithm Analysis in C:Second Edition.北京. 机械工业出版社.2004 [3] 程洁.大话数据结构.北京.清华大学出版社.2020 [4] Stephen Prata.C++ Primer Plus(6th Edition).北京.人民邮电出版社.2020 [5] 刘振安.C语言解惑:指针、数组、函数和多文件编程.北京.机械工业出版社.2016 [6] mandagod.git回退到某个commit.https://blog.csdn.net/mandagod/article/details/110225778 [7] 追求执着.C++文件读写详解(ofstream,ifstream,fstream).https://blog.csdn.net/kingstar158/article/details/6859379