手把手用Python和SQLite打造轻量图书借阅管理系统

发布时间:2026/9/17 3:40:48
手把手用Python和SQLite打造轻量图书借阅管理系统 上个月帮一家社区自习室整理图书借还记录的时候我差点被那本纸质台账劝退。几十页手写记录谁借了什么书、什么时候该还、哪本书已经下落不明全靠管理员一页一页翻。其实这种场景根本不需要上什么大型图书管理系统用 Python 做一个简单的图书借阅管理系统几十分钟就能跑起来而且能把借书、还书、查书的完整链路管得明明白白。今天就把这套方案从头到尾写出来从需求梳理到表结构设计从核心代码到实际运行中才会遇到的边界问题一次性讲透。先说清楚这套东西适合谁。如果你是 Python 基础刚学完函数和类、想找个完整项目练手这篇文章可以当你的第一个实战案例如果你恰好管着一个班级图书角、小书吧或者小型资料室想低成本解决借阅登记问题这套代码可以直接抄走用如果你对数据库设计、事务处理和异常处理感兴趣这篇文章也会有不少值得琢磨的点。我始终觉得管理系统这类项目最大的价值不在于界面多华丽而在于把数据关系理清楚、把业务规则写严谨。很多人一听到管理系统四个字下意识就觉得得上 Django、MySQL、前后端分离。但真实的小型借阅场景里核心诉求就一句话书在哪、在谁手上、什么时候该还。这种需求用 Python 自带的 sqlite3 加一个命令行交互界面就能覆盖本质就是持久化数据加几个增删改查操作。而 SQLite 零配置、单文件存储、Python 内置支持对这类轻量管理场景的匹配度非常高没必要一开始就把技术栈堆得很重。1. 动手之前先理顺借阅系统要解决的本质问题是什么1.1 三个核心角色与他们的真实使用场景在我接触过的借阅场景里参与的人无外乎三类管理员、读者还有被推着去催还书的人。管理员要录入新书、下架旧书、随时查看图书的去向读者要能查书、借书、还书系统本身则要记录每一次借还行为并且能回答两个看似简单但手工很难做到的问题——某一本书现在到底在不在馆某个读者手上还有几本书没还。你可能觉得这些事用 Excel 表格也能做。确实能但 Excel 在多人同时操作、数据一致性这类场景下天然有短板。借书时要在借阅表里加一行、还要在库存表里减一个数两个动作稍微错一步台账就对不上了。而且表格文件一旦被多人传来传去很容易出现版本冲突。用数据库来管理等于把规则交给程序的约束逻辑去执行而不是靠操作者的细心去支撑。1.2 最小可用版本功能清单到底该定多大我当时给自习室定的功能清单非常克制一共六项每一项都对应一个真实操作动作图书入库添加书名、作者、ISBN、出版社和入库数量图书查询按书名或作者模糊搜索实时看到剩余可借数量读者登记给每个读者生成唯一编号登记姓名和电话借书校验读者存在、库存够、没有重复借同一本然后同时完成写借阅记录和扣库存还书把对应的借阅记录标记为已还同时加库存并判断是否逾期查看未还记录列出所有当前仍未归还的借阅记录方便催还你可能注意到了我刻意没有把逾期罚款放进第一版。不是说罚款功能不重要而是第一版应该先把主干链路跑通罚款规则属于典型的边界条件后置处理更合适。如果一上来就想着把所有规则都实现很容易连数据库都还没建明白就被各种 if 分支缠住了。1.3 为什么我坚持用命令行 SQLite而不是一上来就套 Flask这里想认真聊一下选型逻辑。很多新手朋友一上来就想用 Flask 搭个 Web 界面或者用 Tkinter 画个图形界面结果花了两三天调页面布局核心业务逻辑反而没写几行。我的建议很明确业务逻辑永远比界面重要。命令行界面虽然看起来朴素但它能让你把全部注意力放在数据如何流转、状态如何变化上而这恰恰是管理系统的核心。界面终究只是展示层底层逻辑完全可以在切换界面方案时复用。逻辑跑稳了后面再套 GUI 或者迁到 Web成本都很低。数据库选 SQLite 的理由更直接Python 内置模块不需要额外安装服务整个库就是一个文件备份和迁移都极其简单支持标准 SQL 语法还支持事务。对单机运行的小系统来说它比 MySQL 轻得多又比纯文本文件存储安全得多。换句话说这套选型不是不够高级而是为当前场景量身定做的够用方案。2. 表结构设计三张表如何串起借还书的完整生命周期2.1 books 表图书本体与库存状态为什么拆成两个字段动手写代码之前先把表结构想清楚效率会高很多。设计表的顺序也有讲究我习惯先把最底层的实体定下来。图书是系统的核心资源所以先规划 books 表。books 表里我设计这几个字段id 自增主键、isbn 唯一编码、title 书名、author 作者、publisher 出版社、total_count 总库存、available_count 当前可借库存。这里有一个关键设计总库存和可用库存分开存。为什么要拆因为当你想统计这本书一共采购了几本的时候total_count 不应该被借还操作干扰。如果只有一个库存字段还一本加一、借一本减一运行一段时间后它和实际采购数量之间很容易出现偏差而且你根本找不出是哪一笔操作导致的。available_count 会在借书时减一、还书时加一。但注意这个字段只是一个快照状态它只能回答还剩几本回答不了被谁借走了。要还原每一本书的去向必须靠后面那张借阅记录表。2.2 readers 表读者编号的设计比你想的更重要readers 表结构相对简单id 自增主键、reader_no 读者编号、name 姓名、phone 联系电话。这里有一个值得讨论的小细节就是读者编号不要直接用自增 id。我给自习室用的是 R001、R002 这种带前缀的编号好处有两个一是外部沟通时更规范报编号比报第 5 个读者清晰得多二是避免把系统内部的记录数量暴露给用户。你不想让读者根据编号猜到系统里一共登记了多少人虽然这个信息本身不算机密但减少暴露面总归是好习惯。还有一个实际好处是数据迁移。如果未来系统要跟其他平台对接一个稳定的业务编号比数据库自增 id 更利于做映射interfaces之间的对接也会顺畅很多。2.3 borrow_records 表借阅行为的记录与 NULL 的妙用borrow_records 是整个系统里最关键的表它记录每一笔借阅行为。字段包括id 自增主键、book_id 外键指向图书、reader_id 外键指向读者、borrow_date 借出日期、due_date 应还日期、return_date 实际归还日期。return_date 这个字段的设计我特别想强调一下。未归还时它存 NULL归还后存具体的日期字符串。很多新手会习惯用空字符串 表示没有归还但 NULL 在 SQL 里的语义更准确而且查询未还记录时可以直接用WHERE return_date IS NULL过滤逻辑非常直观。反过来统计历史借阅时用WHERE return_date IS NOT NULL同样一目了然。借书时就把 due_date 算好存进库表而不是每次查询时临时计算这个决策也值得说一句。借阅天数规则如果日后调整历史记录里依然保留借书当时约定的应还日期不会因为规则改动而让所有历史记录的逾期判断全部失效。数据表里存事实发生时的值不存当前规则推导出的动态值这是数据建模里很重要的原则。2.4 外键、唯一约束与索引让数据库把住数据质量的关表结构里我还加了两个容易被忽略的东西。第一个是外键约束。SQLite 默认不开启外键检查必须在每次连接时执行PRAGMA foreign_keys ON才能生效。开启之后插入借阅记录时如果 book_id 或 reader_id 不存在数据库会直接拒绝相当于在最后一道关口帮你守住数据引用完整性。第二个是唯一约束isbn 设置 UNIQUE、reader_no 也设置 UNIQUE。这样一来重复录入同一种书时数据库会主动报错不需要你在业务代码里先做一次全表扫描去判断是否存在。索引方面borrow_records 表会频繁按 book_id 和 reader_id 查询所以给这两个字段建索引是值得的。几百条记录可能感觉不到差别但记录到了几千上万条带索引和不带索引的查询速度差距会非常明显。建表的完整 SQL 会在下一章的初始化代码里一起给出。3. 代码落地从建库到一次完整借书流程的逐段拆解3.1 建库与连接两个新手最容易忽略的细节理论部分铺垫完了现在开始写代码。首先是数据库连接和初始化我建议把连接逻辑封装成一个函数不要在每个功能函数里重复写连接代码。import sqlite3 from datetime import datetime, timedelta DB_PATH lib.db def get_conn(): conn sqlite3.connect(DB_PATH) conn.row_factory sqlite3.Row conn.execute(PRAGMA foreign_keys ON) return conn这里有两个细节值得单独拿出来说。第一个是row_factory sqlite3.Row。设置之后查询结果可以通过row[title]按字段名取值而不是靠row[0]这种硬编码下标。字段多的场景下可读性和后续维护成本差别很大。第二个是PRAGMA foreign_keys ON。前面提过这一行必须在每次连接时都执行否则外键约束不会生效借阅记录里就可能出现指向不存在图书的脏数据。接着是初始化建表函数把建表语句都放在这里程序启动时调用一次即可def init_db(): conn get_conn() cur conn.cursor() cur.execute( CREATE TABLE IF NOT EXISTS books ( id INTEGER PRIMARY KEY AUTOINCREMENT, isbn TEXT UNIQUE NOT NULL, title TEXT NOT NULL, author TEXT, publisher TEXT, total_count INTEGER DEFAULT 1, available_count INTEGER DEFAULT 1 ) ) cur.execute( CREATE TABLE IF NOT EXISTS readers ( id INTEGER PRIMARY KEY AUTOINCREMENT, reader_no TEXT UNIQUE NOT NULL, name TEXT NOT NULL, phone TEXT ) ) cur.execute( CREATE TABLE IF NOT EXISTS borrow_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, book_id INTEGER NOT NULL, reader_id INTEGER NOT NULL, borrow_date TEXT NOT NULL, due_date TEXT NOT NULL, return_date TEXT, FOREIGN KEY (book_id) REFERENCES books(id), FOREIGN KEY (reader_id) REFERENCES readers(id) ) ) cur.execute(CREATE INDEX IF NOT EXISTS idx_borrow_book ON borrow_records(book_id)) cur.execute(CREATE INDEX IF NOT EXISTS idx_borrow_reader ON borrow_records(reader_id)) conn.commit() conn.close()建表语句里的 IF NOT EXISTS 是为了重复运行脚本时不报错。日期字段我用 TEXT 类型存YYYY-MM-DD格式的字符串因为 ISO 格式的字符串在排序和比较上和日期类型一致而且可读性好、不会踩时区相关的坑。3.2 图书与读者管理增删改查的标准写法图书入库功能注意参数化查询的写法。我见过不少初学者喜欢用 f-string 拼接 SQL这在本地小工具里也许碰巧能用但一旦未来系统接入了网络这就是一个标准的 SQL 注入漏洞入口。习惯要从小项目开始养成所有用户输入都走占位符。def add_book(isbn, title, author, publisher, total_count): if not isbn or not title or total_count 1: print(录入失败ISBN、书名不能为空库存数量至少为 1) return conn get_conn() try: conn.execute( INSERT INTO books (isbn, title, author, publisher, total_count, available_count) VALUES (?, ?, ?, ?, ?, ?), (isbn, title, author, publisher, total_count, total_count) ) conn.commit() print(f入库成功《{title}》共 {total_count} 本) except sqlite3.IntegrityError: print(录入失败该 ISBN 已存在不要重复添加) finally: conn.close()读者登记功能这里要生成业务编号。最简单的做法是先查当前人数再拼一个新编号def add_reader(name, phone): if not name: print(登记失败姓名不能为空) return conn get_conn() count conn.execute(SELECT COUNT(*) FROM readers).fetchone()[0] reader_no fR{count 1:03d} conn.execute( INSERT INTO readers (reader_no, name, phone) VALUES (?, ?, ?), (reader_no, name, phone) ) conn.commit() print(f读者登记成功编号{reader_no}) conn.close()图书查询用 LIKE 模糊匹配就足够了。这种轻量场景下几百本到几千本的数据量完全不需要引入全文搜索引擎。前后都加%通配符的写法虽然无法利用索引但在小数据量工况下性能差异几乎感知不到优先保证实现简单def search_books(keyword): conn get_conn() rows conn.execute( SELECT id, isbn, title, author, publisher, available_count, total_count FROM books WHERE title LIKE ? OR author LIKE ?, (f%{keyword}%, f%{keyword}%) ).fetchall() conn.close() for row in rows: print(f{row[isbn]} | {row[title]} | {row[author]} | 可借 {row[available_count]}/{row[total_count]})3.3 借书逻辑事务与多重校验缺一不可借书是整个系统含金量最高的一段逻辑因为它涉及两次写操作插入借阅记录和扣减库存。这两个动作必须在一个事务里完成保证要么都成功、要么都失败。def borrow_book(reader_no, isbn): conn get_conn() try: reader conn.execute( SELECT id FROM readers WHERE reader_no ?, (reader_no,) ).fetchone() if not reader: raise ValueError(读者编号不存在请先登记读者信息) book conn.execute( SELECT id, title, available_count FROM books WHERE isbn ?, (isbn,) ).fetchone() if not book: raise ValueError(图书不存在请检查 ISBN) if book[available_count] 0: raise ValueError(f《{book[title]}》已被借完) active conn.execute( SELECT id FROM borrow_records WHERE book_id ? AND reader_id ? AND return_date IS NULL, (book[id], reader[id]) ).fetchone() if active: raise ValueError(该读者已借过这本书归还前不能重复借阅) today datetime.now().strftime(%Y-%m-%d) due_date (datetime.now() timedelta(days30)).strftime(%Y-%m-%d) conn.execute( INSERT INTO borrow_records (book_id, reader_id, borrow_date, due_date) VALUES (?, ?, ?, ?), (book[id], reader[id], today, due_date) ) conn.execute( UPDATE books SET available_count available_count - 1 WHERE id ?, (book[id],) ) conn.commit() print(f借书成功《{book[title]}》应还日期 {due_date}) except ValueError as e: conn.rollback() print(f借书失败{e}) finally: conn.close()这段逻辑里的几个校验点都是真实业务中会遇到的。读者编号不存在时要拦截库存为 0 时要拦截同一个读者重复借同一本书时也要拦截。尤其是最后一条很多人会漏掉。想想这个场景图书馆里有两本相同 ISBN 的书一个读者借走一本后如果系统不检查他可以把另一本也借走甚至在还书之前反复操作把库存全部掏空。所以借书前必须查一次是否存在未归还的同书借阅记录。事务在这里的作用容易被低估。如果插入借阅记录成功、但扣库存失败就会出现借阅记录显示书已借出、实际库存却没减少的严重不一致。反过来扣了库存但没写记录书就凭空消失了。把两步操作放进同一个事务配合异常时的 rollback才能保证数据在任何情况下都保持一致。3.4 还书逻辑状态复原与逾期判断还书是借书的逆操作逻辑上要做两件事把借阅记录标记为已还、把库存加回来。同时计算是否逾期。def return_book(reader_no, isbn): conn get_conn() try: reader conn.execute( SELECT id FROM readers WHERE reader_no ?, (reader_no,) ).fetchone() book conn.execute( SELECT id, title FROM books WHERE isbn ?, (isbn,) ).fetchone() if not reader or not book: raise ValueError(读者或图书不存在) record conn.execute( SELECT id, due_date FROM borrow_records WHERE book_id ? AND reader_id ? AND return_date IS NULL, (book[id], reader[id]) ).fetchone() if not record: raise ValueError(没有找到对应的未归还借阅记录) today datetime.now().strftime(%Y-%m-%d) conn.execute( UPDATE borrow_records SET return_date ? WHERE id ?, (today, record[id]) ) conn.execute( UPDATE books SET available_count available_count 1 WHERE id ?, (book[id],) ) conn.commit() due datetime.strptime(record[due_date], %Y-%m-%d) today_dt datetime.strptime(today, %Y-%m-%d) if today_dt due: overdue_days (today_dt - due).days print(f还书成功该书已逾期 {overdue_days} 天) else: print(还书成功按时归还) except ValueError as e: conn.rollback() print(f还书失败{e}) finally: conn.close()注意这里判断逾期用的是today_dt due也就是应还日期当天归还依然算按时。这个规则是一个业务约定不同场景可能有不同标准但在代码里务必明确下来不要搞成隐性的边界条件。如果哪天规则改成提前一天归还才算按时只需要改动这一个比较符号。3.5 查询未还记录与主菜单把功能串成可用系统在收尾之前把当前未还记录这个查询也写一下。这个功能对管理员来说几乎是每天都要看的它回答了系统里最核心的问题哪些书在外面、在谁手上、什么时候到期。def list_active_borrows(): conn get_conn() rows conn.execute( SELECT r.reader_no, r.name, b.title, br.borrow_date, br.due_date FROM borrow_records br JOIN readers r ON br.reader_id r.id JOIN books b ON br.book_id b.id WHERE br.return_date IS NULL ORDER BY br.due_date ).fetchall() conn.close() if not rows: print(当前没有未归还的记录) return for row in rows: print(f{row[reader_no]} | {row[name]} | {row[title]} | 借于 {row[borrow_date]} | 应还 {row[due_date]})最后用主菜单把所有功能串起来。主菜单不需要搞复杂一个 while 循环加 input 分发就行。我建议这个版本用 if-elif 结构即可等你想优化交互时再考虑用字典映射函数名那是另一个层面的改进。def main(): init_db() while True: print(\n 图书借阅管理系统 ) print(1. 添加图书) print(2. 登记读者) print(3. 借书) print(4. 还书) print(5. 搜索图书) print(6. 当前未还记录) print(7. 退出) choice input(请选择操作) if choice 1: isbn input(ISBN) title input(书名) author input(作者) publisher input(出版社) total int(input(入库数量)) add_book(isbn, title, author, publisher, total) elif choice 2: name input(姓名) phone input(电话) add_reader(name, phone) elif choice 3: reader_no input(读者编号) isbn input(ISBN) borrow_book(reader_no, isbn) elif choice 4: reader_no input(读者编号) isbn input(ISBN) return_book(reader_no, isbn) elif choice 5: keyword input(输入书名或作者关键词) search_books(keyword) elif choice 6: list_active_borrows() elif choice 7: print(系统已退出) break else: print(无效选项请重新输入) if __name__ __main__: main()一段完整的运行效果大概是这样 图书借阅管理系统 请选择操作1 ISBN978-7-111-12345-6 书名Python编程从入门到实践 作者Eric Matthes 出版社人民邮电出版社 入库数量3 入库成功《Python编程从入门到实践》共 3 本 请选择操作3 读者编号R001 ISBN978-7-111-12345-6 借书成功《Python编程从入门到实践》应还日期 2026-02-19到这里一个能用的系统雏形已经完整了。但项目做出来容易真正把它做好还需要面对几个隐藏在细节里的边界场景。4. 最容易翻车的三个边界场景日期、重复借阅与脏数据4.1 日期计算为什么直接相减会多算一天逾期计算是最容易踩坑的地方。如果你在存借阅日期时用了带时分秒的完整时间戳比如datetime.now()直接存进去而计算逾期时又用了date.today()那么两个类型相减得到的结果会包含时间差的部分最终算出的天数很可能比实际天数多一天或少一天具体取决于读者在一天中的哪个时刻还书。我推荐的统一做法是存的时候用strftime(%Y-%m-%d)只保留日期部分取出来比较时用datetime.strptime(date_string, %Y-%m-%d).date()转成 date 类型再相减。这样计算完全基于自然日不带任何时间因素结果稳定可预测。代码里所有涉及日期的读写都沿用这一套格式就不会出现昨天借的隔了一天还书结果算成逾期两天这种让管理员没法跟读者解释的问题。4.2 重复借阅的取舍规则越简单边界越清晰前面代码里已经实现了同一读者不能重复借同一 ISBN 图书的校验但这里还想把背后的取舍讨论清楚。现实中同一 ISBN 往往对应多本完全相同的书比如自习室采购了 5 本同一版次的《Python编程从入门到实践》。一个读者借走一本以后他能不能再把剩下几本也借走这其实是个业务规则问题没有标准答案。我默认选择禁止理由是管理系统的首要目标是降低混乱度。如果允许一个人反复借同一本书那么借阅记录会变得很分散催还时还要跟同一个读者催好几本同名的书沟通成本很高。规则简单边界就清晰出 bug 的概率自然低。如果你的场景确实需要放开这个限制只需要把 borrow_book 里查 active 记录的那段逻辑去掉或放宽条件。但放开之后必须同步考虑库存竞争问题假设只有 3 本书一个人全借走其他读者就无书可借了。要不要限制单人最大借阅数这又是一个新的规则。所以说规则越简单你需要处理的边界情况就越少。4.3 脏数据拦截入口校验与数据库约束的双保险图书管理系统的使用者多半不是程序员他们完全可能手滑输入负数库存、空书名或者把 ISBN 填成一串毫无意义的字符。对于这类脏数据我的处理原则是数据库约束兜底 入口处提示双管齐下。数据库层用 NOT NULL、UNIQUE 这类约束做最后一道防线就算业务代码漏写了某些判断数据库也会拦下明显不合法的数据。入口处在写入之前做基础校验比如 add_book 里先判断书名是否为空、库存是否小于 1。校验逻辑要写成抛出异常的方式然后在调用方统一捕获处理不要在功能函数里写一长串 if-else 加 print 然后提前 return那样代码会越写越乱错误信息也无法统一管理。至于 ISBN 本身的真实性校验真实世界里有标准的校验位算法可以判断一个 ISBN 是否合法。但小型系统第一版可以先不做那么严格至少保证非空、长度合理即可。不要在这上面过度设计毕竟这不是图书电商平台录入几本书的信息量不会大到失控。5. 从能跑的 demo 到实用工具实测心得与扩展建议5.1 实测中的意外与迭代一份来自真实使用的验收报告把系统交给自习室用了一周之后我最深的体会是用户反馈最多的往往不是功能缺失而是操作路径不直观。比如管理员一开始会抱怨我怎么知道哪些书被借走了其实系统里有未还记录查询但在命令行菜单里藏得比较深使用频率又高导致他们觉得不好用。后来我把当前未还记录从选项 6 提前到选项 2并且让借书成功时自动打印一条未还摘要实际使用体验立刻好了很多。另一个实测教训是备份问题。SQLite 整个库就是一个文件备份只需要复制这一个文件。我写了一个简单的批处理脚本每天定时把 lib.db 复制到一个带日期的备份目录保留最近 30 天。这个操作成本几乎为零但关键时刻能救命。数据量小并不代表数据不重要若干天的借阅记录一旦丢失恢复起来全靠手写台账那是灾难级的体验。5.2 扩展方向GUI、Web 和数据可视化怎么加核心逻辑稳定之后想怎么套壳都可以。我给三个扩展方向排个优先级。第一个方向是图形界面。用 Tkinter 给系统加一个窗口界面核心业务函数一行都不用改只是在按钮事件里调用已有的 borrow_book、return_book 这些函数。Tkinter 是 Python 内置模块不需要额外安装非常适合作为初学者的第一个 GUI 练手项目。第二个方向是改成 Web 应用。用 Flask 把原来的功能函数包装成 API 接口返回值从 print 改成 JSON前端用一个简单的 HTML 表单页面就能让多人通过浏览器访问。这个方向能把你对后端服务的理解往前推一大步为后续接触更复杂的框架打下基础。第三个方向是数据可视化。用 matplotlib 或 pyecharts 画出借阅量 Top 10 图书、每月借阅趋势这类图表。哪怕只是几张简单的柱状图和折线图都能让整个系统的专业感提升不少。而且这些数据分析可以直接基于 borrow_records 表做 SQL 聚合查询不需要额外维护任何统计字段。5.3 自己动手复刻时五个最值得较真的细节最后分享几个我觉得最值得较真的细节这些都是自己动手敲一遍才能理解的。第一一定要亲手跑一遍PRAGMA foreign_keys ON和注释掉这行代码的区别。试着插入一条指向不存在图书的借阅记录看看数据库在两种情况下分别是什么反应你对外键约束的理解会上一个台阶。第二试着在借书流程中故意制造一个异常比如传一个不存在的读者编号然后观察事务回滚后数据库里的数据状态。第三连续两次借同一本书验证重复借阅拦截是否生效。第四把电脑系统时间改到应还日期之后测试逾期判断逻辑是否准确。第五检查一下数据库文件 lib.db 的大小和内容变化用 SQLite 可视化工具打开它看看三张表的数据是怎么关联起来的。这些操作没有一个是多余的它们能帮你在真正的业务场景里从容应对各种意外。这套代码虽然简单但把它吃透你收获的远远不止一个图书借阅管理系统。数据库表设计、事务处理、参数化查询、异常处理、输入校验这一整套工程化思维恰恰是从 Python 入门走向项目实战之间最难自学的一段路。下次不管你要管理的是书、设备还是资产换个表结构就能复用这套代码骨架。这也正是我为什么一直建议初学者认认真真做一遍这个小系统的原因。