news 2026/9/20 8:13:57

图书采购借阅系统架构说明书:4+1视图与SSH落地

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
图书采购借阅系统架构说明书:4+1视图与SSH落地

简介:这是一份面向图书杂志采购与借阅系统的软件架构设计说明书,以单份 Word 文档呈现,适合软件工程课程设计、架构入门与项目文档写作参考。文档围绕系统架构展开,从简介、架构表示方式、设计目标与约束,到用例视图、逻辑视图、进程视图、实施视图等模块逐层说明,并涵盖关键功能需求、质量需求、开发策略、接口通信与非功能性需求等内容,便于项目经理、程序设计员和测试人员理解系统蓝图与职责边界。压缩包内共 1 个 docx 文件,约 367KB,体量轻便,可直接查阅目录与章节。已有 1439 人学习下载,说明其在同类架构文档中具有较高参考热度。读者可借助其中多视图建模思路、架构决策记录与评估方法,对照完成需求分析、模块划分和测试框架设计,尤其适合需要撰写架构说明书或梳理分层结构的学习者借鉴。

1. 图书采购借阅系统的架构说明书,为什么比源码更值得先拆

接手一个 2010 年前后的 Java Web 项目,多数人的第一反应是直奔 src 目录翻代码,结果在两三百个文件里迷路,还搞不清哪个包该改。图书杂志采购和借阅系统这类项目,信息密度最高的地方其实是一份叫《软件架构设计说明书》的 docx:它把系统切成用例视图、逻辑视图、进程视图、实施视图、部署视图五份,每一份只回答一类涉众的问题,项目经理、程序员、测试设计员各取所需,互不干扰。

这份文档适合三类人看:要做毕业设计却不知道架构章节怎么落笔的学生,要接手遗留 SSH 项目却看不懂包命名规则的维护者,以及需要把"稳定、安全、便捷"这类形容词翻译成可验收指标的工程师。

最该先看的是第三节"架构设计目标与约束"里的三行数字——查询响应不超过 10 秒,其余交互不超过 3 秒,平均故障间隔时间不低于 200 小时。这三个数字不是装饰,它们直接决定了逻辑视图为什么要把查询单独抽一层、部署视图为什么必须把数据库和 Web 容器拆开。先读这三行,再回头看视图,整份说明书就顺了。

2. 用 4+1 视图把架构说明书拆成五个可验证的切面

2.1 五视图各自的读者和产出物

架构文档最容易写崩的地方,是把五个视图写成五份互相抄的废话。判断标准很简单:每个视图必须对应一个具体的产出物,且这个产出物只有这个视图能给。下表是照着这份说明书的章节结构整理出来的对应关系,写文档时可以直接当检查表用。

视图回答的核心问题主要读者必须产出的内容
用例视图系统对外提供哪些可观察行为客户、测试设计员参与者清单、关键用例说明
逻辑视图内部由哪些包和子系统组成程序设计员层次模型、设计包清单
进程视图运行时刻进程与线程如何交互程序设计员、性能测试角色进程消息序列
实施视图代码如何映射到构件与配置管理配置管理员实施模型、模块归属
部署视图软件构件跑在哪些物理节点上运维、项目经理节点拓扑、部署方案

关键点在于层次:用例视图决定逻辑视图的包划分,逻辑视图决定进程视图里哪些调用要跨进程,进程视图和部署视图共同决定质量指标能不能达标。顺序颠倒过来写,文档基本就要返工。

2.2 用例视图里的五类参与者与权限边界

这份说明书把参与者分成游客、读者、图书管理员、采购管理员、系统管理员五类。抽用例时不要对着需求文档逐条抄,按下面的顺序走一遍,能省掉大量返工:

  1. 圈出需求文档里所有动宾短语:借书、还书、续借、罚款、订购、取消订购、验收确定、编目入库、预约。
  2. 按操作对象归类,同一对象的动词合并成一个用例,比如"续借"和"还书"都归到流通管理。
  3. 标出每个用例的主参与者与触发条件,借书的前置条件是证件有效且无超期。
  4. 反向检查:是否存在某个角色能覆盖全部用例。系统管理员就是典型例外,他只管管理员账号,不碰图书业务。

判断完边界,落到数据库就是一张角色权限关系。早期项目常见做法是直接在用户表里放一个 role 字段,结果加一个"采编人员"就要改代码。

-- 把用例视图里的五类参与者落成三张表,避免角色硬编码在 Java 里 CREATE TABLE sys_role ( role_id INT PRIMARY KEY AUTO_INCREMENT, role_code VARCHAR(32) NOT NULL UNIQUE COMMENT 'GUEST/READER/LIB_ADMIN/PUR_ADMIN/SYS_ADMIN', role_name VARCHAR(64) NOT NULL ); CREATE TABLE sys_permission ( perm_id INT PRIMARY KEY AUTO_INCREMENT, perm_code VARCHAR(64) NOT NULL UNIQUE COMMENT '如 book.query / loan.renew / purchase.instore' ); CREATE TABLE role_permission ( role_id INT NOT NULL, perm_id INT NOT NULL, PRIMARY KEY (role_id, perm_id) ); -- 借阅记录按读者加状态建联合索引,直接服务“查看借阅/归还信息”的 3 秒约束 CREATE INDEX idx_loan_reader_status ON loan_record (reader_id, loan_status);

三张表的分工是:sys_role存角色定义,sys_permission存原子权限,role_permission做多对多关联。perm_code用点号分段,前缀对应模块,方便在拦截器里做通配匹配。联合索引的顺序不能反,reader_id在前是因为查询一定带读者身份,loan_status在后做范围过滤,反过来建索引就只能用到第一列。

2.3 从关键质量需求倒推架构约束

质量需求如果不翻译成技术动作,就等于没写。"查询不超过 10 秒"这条,在图书表几万条数据时其实很容易达标,但借阅排行榜、新书通报、藏书多条件查询是三条不同的 SQL,任何一条走了全表扫描都会拖垮整体。对应动作是给查询类接口单独走一个只读数据源,并在book表的titleisbncategory_id上按实际查询组合建索引。

"其他交互不超过 3 秒"约束的是写操作之外的所有请求,包括登录、注册、改个人信息。这类请求的共同点是单表操作加会话校验,慢下来的原因通常是登录后每次请求都重新查一遍权限。缓解方式是登录时把权限码集合放进会话,拦截器只读会话不查库。

"MTBF 不低于 200 小时"则决定了部署视图不能把应用和数据库塞在同一台机器。200 小时约等于八天多,一个教学规模的项目平均八天出一次故障,说明单点故障必须被隔离,数据库进程崩溃不能把 Web 容器一起带走。

提示:这三条指标写在文档里时要带上测量口径,比如"查询响应时间指服务端处理时间,不含网络传输",否则验收时双方各说各话。

3. 逻辑视图落地:Struts+Spring+Hibernate 的包结构与装配配置

3.1 四个基础包的职责切分

说明书里给出了bpms.actionbpms.actionFormbpms.dbbpms.domain四个基础包,这个划分是 SSH 时代最典型的分层法,但只给包名不给依赖规则,代码照样写乱。补上依赖方向之后才是可执行的约束。

包名放什么允许依赖一旦出现就说明分层破了
bpms.actionStruts Action,参数接收与页面跳转actionForm、service直接写 JDBC 或 HQL
bpms.actionForm表单对象,纯数据载体任何业务判断
bpms.domain实体类与 Hibernate 映射依赖 action 或 db
bpms.dbDAO 与 Hibernate 会话操作domain页面跳转、request 取值

依赖只能单向:action 依赖 service,service 依赖 db,db 依赖 domain。反向依赖一次都不要开,尤其是让 domain 引用 action,编译期看着没事,打包时会带出整个 Web 层。

3.2 Struts 与 Spring 的装配配置

Struts 负责路由和表单绑定,Spring 负责对象生命周期和事务,两者靠DelegatingActionProxy接起来。这个代理类是整套配置的关键,它让 Struts 不去 new Action,而是按path去 Spring 容器里找同名 bean。

<!-- struts-config.xml:只做路由,业务全部下沉到 Service --> <struts-config> <form-beans> <form-bean name="bookSearchForm" type="bpms.actionForm.BookSearchForm"/> </form-beans> <action-mappings> <action path="/bookSearch" type="org.springframework.web.struts.DelegatingActionProxy" name="bookSearchForm" scope="request" validate="false"> <forward name="list" path="/WEB-INF/jsp/book/list.jsp"/> </action> </action-mappings> </struts-config>
<!-- applicationContext.xml:bean 名必须与 struts 的 path 一致,否则启动即报找不到 Action --> <bean name="/bookSearch" class="bpms.action.BookSearchAction"> <property name="bookService" ref="bookService"/> </bean> <bean id="bookService" class="bpms.service.impl.BookServiceImpl"> <property name="bookDao" ref="bookDao"/> </bean> <!-- 声明式事务:按方法名前缀决定传播行为和只读标记 --> <bean id="txProxyTemplate" abstract="true" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <property name="transactionManager" ref="transactionManager"/> <property name="transactionAttributes"> <props> <prop key="query*">PROPAGATION_REQUIRED,readOnly</prop> <prop key="save*">PROPAGATION_REQUIRED</prop> <prop key="*">PROPAGATION_REQUIRED</prop> </props> </property> </bean>

scope="request"表示表单对象每个请求新建,避免上一个用户的数据残留到下一个用户;validate="false"是关掉 Struts 自带的校验框架,改由 Spring 侧的校验器统一处理,两套校验同时开着最容易出现"页面提示通过、后台报错"的诡异现象。事务属性里query*readOnly,让 Hibernate 跳过脏检查,查询类接口能省下可观的 CPU。PROPAGATION_REQUIRED表示有事务就加入、没有就新建,写操作必须用这个,改成SUPPORTS会导致部分写操作没有事务包裹。

3.3 Hibernate 映射与事务边界

实体映射别偷懒全用注解,bookcategoryloan_recordreader这几组关系在 hbm.xml 里显式写出抓取策略更可控。默认的懒加载在页面渲染时才触发查询,一旦 Action 里提前把 Session 关了,就是经典的LazyInitializationException

// BookDaoImpl:查询方法不手动开事务,交给 Spring 代理 public class BookDaoImpl extends HibernateDaoSupport implements BookDao { public List<Book> queryByCondition(BookSearchForm form, int pageNo, int pageSize) { // 用 HQL 拼条件,禁止把表单字段直接字符串拼接进 SQL StringBuilder hql = new StringBuilder("from Book b where 1=1 "); Map<String, Object> params = new HashMap<String, Object>(); if (StringUtils.hasText(form.getTitle())) { hql.append("and b.title like :title "); params.put("title", "%" + form.getTitle().trim() + "%"); } if (form.getCategoryId() != null) { hql.append("and b.categoryId = :categoryId "); params.put("categoryId", form.getCategoryId()); } hql.append("order by b.createTime desc"); Query query = getSession().createQuery(hql.toString()); for (Map.Entry<String, Object> e : params.entrySet()) { query.setParameter(e.getKey(), e.getValue()); } query.setFirstResult((pageNo - 1) * pageSize); // 分页起点,从 0 开始 query.setMaxResults(pageSize); // 每页条数,建议固定 20 return query.list(); } }

setFirstResult的算法是(页码 - 1) × 每页条数,页码从 1 开始传,别从 0 开始,否则第一页会变成空。like条件用参数占位符而不是拼接,既防注入也让 Hibernate 能复用执行计划。分页条数固定成常量而不是让页面传,是为了防止有人传 10000 把库拖死。

3.4 分层落地常踩的三个坑

第一个是 N+1 查询。列表页显示每本书的类别名,如果类别是懒加载,20 本书就是 21 条 SQL。解法是在 hbm.xml 里对该关联加fetch="join",或在 HQL 里写left join fetch

第二个是 ActionForm 跨请求复用。把 scope 设成 session 之后,用户 A 的查询条件会出现在用户 B 的页面上,这是权限事故的高发点。

第三个是事务自调用失效。Service 内部用this.saveXxx()调自己的另一个方法,Spring 的 AOP 代理拦不到,事务配置直接作废。要么抽成独立的 DAO 方法,要么注入自身代理。

4. 进程视图走通三条主链路:查询、注册与采购入库的调用时序

4.1 搜索图书链路的六步消息

说明书里画的搜索链路是主界面向后台发请求、后台取数据、数据库返回、再层层回传,一共六步。落到代码上,这六步压缩成一次 Action 调用加一次 Service 查询,真正需要关注的是每一步失败时回传什么。

// BookSearchAction:接收分页参数,异常统一转成状态信息返回页面 public class BookSearchAction extends DispatchAction { private BookService bookService; public ActionForward query(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) { BookSearchForm searchForm = (BookSearchForm) form; int pageNo = searchForm.getPageNo() == null ? 1 : searchForm.getPageNo(); try { PageResult<Book> page = bookService.queryByCondition(searchForm, pageNo, 20); request.setAttribute("page", page); request.setAttribute("status", "SUCCESS"); return mapping.findForward("list"); } catch (Exception e) { // 不把堆栈抛给页面,只回一个状态码,详细日志落 log4j log.error("book search failed, form=" + searchForm, e); request.setAttribute("status", "QUERY_ERROR"); return mapping.findForward("list"); } } public void setBookService(BookService bookService) { this.bookService = bookService; } }

pageNo为空时兜底成 1,防止第一页就报空指针。异常不往上抛而是转成QUERY_ERROR状态码,对应进程视图里那条"状态信息(成功与否)"的回传消息,页面拿到状态码只显示一句提示,不暴露数据库结构。日志里带上完整表单对象,排查时不用再问用户输入了什么。

分页参数怎么定,直接影响 10 秒那条指标。经验值如下表。

参数建议值说明
pageSize20列表页可视范围,超过 50 单页渲染会明显变慢
最大页深500深度分页改用按主键游标,setFirstResult到几万会全表扫
查询超时8 秒留在 10 秒指标之内,超时直接中断并回状态码
缓存范围排行榜类新书推荐、借阅排行按小时缓存,藏书查询不缓存

4.2 注册与改资料的状态回传为什么要统一

游客注册和读者修改个人信息,在进程视图里是两条几乎一样的消息序列:填表、提交、写库、回传状态。这两个用例的差异只在目标实体和权限校验,所以状态码必须共用一套枚举,否则页面要写两套判断逻辑。

// 统一状态码:注册、改资料、采购入库共用,页面只需处理这几种 public enum ResultCode { SUCCESS, // 操作成功 PARAM_INVALID, // 必填项缺失或格式错误 DUPLICATE, // 账号、ISBN 等唯一键冲突 NO_PERMISSION, // 会话失效或角色不符 SYSTEM_ERROR // 数据库或未知异常 }

DUPLICATE单独拎出来,是因为账号重复和 ISBN 重复在页面上要给出不同的跳转引导。NO_PERMISSIONSYSTEM_ERROR分开,前者回登录页,后者只刷新当前页并提示稍后重试,混在一起用户会被莫名踢下线。

4.3 采购入库的状态流转

采购管理模块是这份说明书里状态最多的一条链路:图书订购、取消订购、验收确定、编目入库,捐赠和交换来的图书要先清点确认再编目。状态不能只用一个字段表示,否则分不清"已验收但未编目"和"已编目但未上架"。

当前状态触发动作数据变更是否可回退
订购中取消订购状态置为已取消,释放预算占用可回退
订购中验收确定数量、单价、实收册数写入明细不可
已验收编目入库生成图书主记录,分配条码与分类号不可
已入库清点确认捐赠/交换图书补录来源字段不可
修补中重新上架状态回可借,同时解除流通冻结可回退

"修补中"这个状态容易被漏掉,它属于流通管理而不是采购,但会直接影响借书权限判断——处于修补中的图书,即使库存字段显示有货,也必须拒绝借出。

4.4 借书权限校验的判定顺序

说明书明确列出了借书时要自动区别的因素:超期、未交罚款、证件有效期、预约、其他违规。这五项的判定顺序不能随意调,因为它们的处理动作不同,有的要跳罚款页,有的只是拒绝。

// 借书前置校验:短路返回,顺序即优先级 public ResultCode checkBorrowPermission(Reader reader, Book book) { if (reader == null || !reader.isCardValid()) { return ResultCode.NO_PERMISSION; // 证件过期,最先判,后续查询无意义 } if (reader.getUnpaidFine() != null && reader.getUnpaidFine() > 0) { return ResultCode.NO_PERMISSION; // 有未缴罚款,直接拒 } if (reader.getOverdueCount() > 0) { return ResultCode.NO_PERMISSION; // 有超期未还,先还书再借 } if (book.getStatus() == BookStatus.REPAIRING) { return ResultCode.PARAM_INVALID; // 修补中,页面提示改借其他书 } if (book.getAvailableCopies() <= 0) { return ResultCode.DUPLICATE; // 无可借副本,引导走预约 } return ResultCode.SUCCESS; }

证件有效性排第一,因为证件过期时读者身份本身就不成立,后面几项查了也是白查。罚款和超期都返回NO_PERMISSION,但页面提示词不同,实现时可以在 ResultCode 外再挂一个 message key。可借副本为零时返回DUPLICATE并引导预约,而不是简单拒绝,这条对应说明书里"预约处理"这个用例。

5. 质量约束转验收项:3 秒响应与 200 小时 MTBF 怎么测

文档写完不算完,架构说明书里的指标要能被复测才有意义。三个数字里最容易糊弄的是响应时间,最常见的做法是用ab或 JMeter 打一轮,然后拿平均值交差。平均值会掩盖长尾,验收要看的是 95% 分位。

# 对查询接口压测,-n 总请求数,-c 并发数,输出里直接看 95% 那行 ab -n 2000 -c 50 "http://localhost:8080/bpms/bookSearch.do?method=query&pageNo=1" # 只读接口连打三轮,取最差的一轮作为验收数据,避免缓存命中造成假象 for i in 1 2 3; do ab -n 1000 -c 30 "http://localhost:8080/bpms/bookSearch.do?method=query&pageNo=1" \ | grep -E "95%|Failed" done

-c 50是并发连接数,压测值参考的是同时在线阅读的用户规模,不是注册用户总数。三轮取最差是因为第一轮往往命中了 Hibernate 二级缓存,数据虚高。Failed requests只要不为零就要先查原因,连接超时和业务异常在ab的输出里是混在一起的。

10 秒那条指标还要配合慢查询日志验证,否则压测时数据量太小,上线必炸。

-- 开慢查询,阈值定 2 秒,留出到 10 秒之间的排查余量 SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 2; -- 对藏书多条件查询做执行计划检查,重点看 type 和 rows 两列 EXPLAIN SELECT b.book_id, b.title, b.author FROM book b WHERE b.category_id = 12 AND b.title LIKE '软件%' ORDER BY b.create_time DESC LIMIT 20;

type出现ALL就是全表扫描,rows超过五位数基本要加索引。title上的前置模糊匹配用不到索引,所以这类查询要么限定分类先缩小范围,要么依赖单独的搜索引擎,别指望在 MySQL 里硬扛。

MTBF 不低于 200 小时属于运行期指标,测法是采日志里的启动时间戳,算两次启动之间的间隔。

# 从日志中提取应用启动时间,计算相邻两次启动的间隔(单位:小时) grep "Application startup complete" app.log \ | awk '{print $1" "$2}' \ | awk 'NR>1 {cmd="date -d \""$0"\" +%s"; cmd | getline t; close(cmd); print (t-prev)/3600 " 小时"; prev=t} NR==1 {cmd="date -d \""$0"\" +%s"; cmd | getline t; close(cmd); prev=t}'

间隔时长小于 200 的记录都要单独看一次堆栈,常见原因是连接池耗尽和OutOfMemoryError。连接池上限如果按默认的 8 配,并发 50 时就会大面积等待超时,这个值和压测并发数要一起定。

文档本身的交付也有坑。这类架构说明书通常以 docx 形式流转,用 WPS 打开时目录域和交叉引用容易错位,建议定稿后另存一份 PDF 作为评审版本,docx 只留作可编辑源文件。图表编号改成手动维护的固定文本,别依赖自动编号,否则任何一次增删章节都会让引用全部错位。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 8:13:09

Three.js实现3D模型动画展示与性能优化实战

1. 项目概述&#xff1a;Three.js 3D模型动画展示最近在开发一个基于Three.js的3D模型动画展示项目&#xff0c;这个开源方案特别适合需要展示产品三维效果、建筑可视化或游戏角色动画的场景。不同于静态的3D展示&#xff0c;这个项目实现了模型加载、材质控制、动画播放和交互…

作者头像 李华
网站建设 2026/9/20 8:12:24

数据中心供电制式变革:HVDC如何成为算力时代的破局点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 8:09:48

Python+Django+Vue.js构建建筑工程任务管理系统实践

1. 项目概述与背景建筑工程项目管理一直是行业痛点&#xff0c;传统Excel表格和纸质文档管理方式效率低下&#xff0c;任务分配混乱、进度跟踪困难、资源调配失衡等问题频发。作为一名长期从事工程管理软件开发的从业者&#xff0c;我决定采用PythonDjangoVue.js技术栈构建一套…

作者头像 李华
网站建设 2026/9/20 8:09:29

ARM7指令集入门:寄存器、寻址与流水线核心解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 8:09:06

MQTT核心机制实战:发布订阅、QoS、遗嘱消息与持久会话详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华