
这段时间又带了一期软件方向的“项目实训”每天看着大家在群里报Bug、改需求、掉头发自己也攒了一大堆现场记录。这篇就把这一整轮实训的完整过程和我的个人记录整理出来不聊虚的全部是能直接拿来用的步骤、代码片段和避坑经验。无论你是正在做实训的大学生、准备课程设计的初学者还是带实训项目的老师这篇应该都能帮你少走一些弯路。这一期我选的是Java Web方向的“图书管理系统”一个很老但非常典型的实训题目。之所以选它是因为它功能清晰、边界明确能覆盖登录权限、增删改查、分页检索、事务处理、部署上线这些核心知识点又不会因为业务太复杂把初学者直接劝退。下面的内容就是围绕这个项目展开的完整记录包括选题逻辑、技术选型、核心实现、测试部署以及我个人最看重的复盘部分。1. 实训项目怎么选比怎么写代码更值得花时间1.1 选题原则为什么我劝你别一上来就做“大系统”每次实训开班总有同学说“我想做一个电商平台”还有人想做“校园社交App”。想法是好的但实训周期通常只有两到四周一个人或者三五人小组要在这么短的时间里把一个电商系统做完结果往往是注册登录做得像模像样订单和支付却成了烂尾楼。选题的第一条原则是“需求边界够清晰”。图书管理系统的借书、还书、检索、读者管理每一条需求都能在几天内看到可运行的结果这种正反馈对新手保持信心很重要。第二条原则是“能覆盖本阶段的核心知识点”如果选得太简单比如只做一个静态展示页那就失去实训的意义了。第三条原则是“有明确的扩展空间”图书系统做完可以加预约、超期罚款、统计报表这些扩展点能留给学有余力的人继续折腾。我一直和学生们说实训不是做产品实训是练基本功。一个能在两周内完整跑起来、拿得出手的项目比一个做了一半就停摆的“大平台”有用得多。1.2 需求拆解用用户故事把功能列表变成数据库字段很多人拿到题目后直接打开数据库建表这是一个非常容易踩坑的习惯。正确做法是先写用户故事把每一个操作场景写清楚再从中提取功能列表。图书管理系统的用户故事可以这么写作为读者我希望输入关键词就能查到图书并看到剩余可借数量。作为读者我希望登录后可以借书、续借、还书并查看自己的借阅记录。作为管理员我希望可以新增、修改、删除图书信息管理读者账号。作为管理员我希望可以看到当前逾期未还的记录方便催还。把这些故事整理成功能清单后基本就是下面这张表功能模块具体功能优先级涉及角色用户管理注册、登录、密码修改高读者、管理员图书管理图书新增、修改、删除、上下架高管理员图书检索按书名、作者、ISBN模糊查询高所有人借阅管理借书、还书、续借、借阅记录查询高读者、管理员统计报表借阅量统计、逾期列表中管理员这个阶段其实还有一件额外收获当你把用户故事和功能清单整理好数据库的表结构、表之间的关系也就浮出水面了。功能模块之间的归属关系、一对多还是多对多全都能对应到表设计里。所以我会建议实训开始后先别急着敲代码花一到两天做需求文档后面会省出三到四天的返工时间。1.3 数据库设计表关系先想清楚再动手图书管理系统的表数量不多一般四到五张核心表就能撑起整个业务。user用户表区分管理员和普通读者。book图书表保存书名、作者、ISBN、分类、库存等基础信息。category分类表与图书形成一对多关系。borrow_record借阅记录表连接用户和图书保存借书时间、应还时间、实际归还时间和状态。以图书表为例字段可以这样设计CREATE TABLE book ( id BIGINT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(32) NOT NULL UNIQUE COMMENT 国际标准书号, title VARCHAR(128) NOT NULL COMMENT 书名, author VARCHAR(64) DEFAULT COMMENT 作者, publisher VARCHAR(128) DEFAULT COMMENT 出版社, category_id BIGINT DEFAULT NULL COMMENT 分类ID, total_count INT NOT NULL DEFAULT 0 COMMENT 总藏书量, available_count INT NOT NULL DEFAULT 0 COMMENT 当前可借数量, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_category (category_id), KEY idx_title (title) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书表;借阅记录表是最容易出现设计问题的很多人会漏掉“续借次数”和“状态”这两个字段。没有续借次数就没法限制读者续借几次没有状态区分就没法快速判断一条记录是借出中、已归还还是已逾期。CREATE TABLE borrow_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 读者ID, book_id BIGINT NOT NULL COMMENT 图书ID, borrow_time DATETIME NOT NULL COMMENT 借出时间, due_time DATETIME NOT NULL COMMENT 应还时间, return_time DATETIME DEFAULT NULL COMMENT 实际归还时间, renew_count INT NOT NULL DEFAULT 0 COMMENT 已续借次数, status TINYINT NOT NULL DEFAULT 0 COMMENT 0借出 1已还 2逾期, KEY idx_user (user_id), KEY idx_book (book_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT借阅记录表;这里有个实际经验所有字段尽量加COMMENT注释特别是实训项目需要交文档的时候数据库注释能直接复制到设计说明书里。索引不要贪多业务查询主要关注user_id、book_id、status这几个维度一张表三到四个索引已经足够。2. 技术选型与开发环境搭建别为了最潮技术赔上实训周期2.1 技术栈取舍这套组合最不容易翻车技术选型是实训里最容易起争执的地方。有学生一上来就要用微服务有人非要搞前后端完全分离加分布式缓存我不能说这些方向不对但在两周到四周的实训周期里复杂度就是最大的敌人。我推荐的组合非常简单但非常稳层级选型理由后端框架Spring Boot 2/3自动配置成熟开箱即用持久层MyBatisSQL可控适合教学理解数据库MySQL 8.x主流资料多前端Thymeleaf Bootstrap不分离减少接口联调成本构建工具Maven生态成熟部署方式打jar包 systemd单机部署简单直接有人可能会说都什么年代了还用服务端渲染我做实训通常要求“先跑通再扩展”。前后端分离意味着要同时维护前端工程、后端接口、联调、跨域一堆问题对刚开始接触完整项目的人来说负担太重。Thymeleaf加Bootstrap虽然看起来朴素但可以让注意力集中在后端业务逻辑上。等实训结束想提升再自己拆成前后端分离也不迟。2.2 环境初始化与项目结构规范环境搭建这一步几乎每个实训班都会卡住一批人。最常见的三个问题依次是Maven下载依赖太慢、MySQL字符集不对、Spring Boot版本与JDK不匹配。Maven加速的办法是配置阿里云镜像在settings.xml里加一个mirror。JDK和Spring Boot的对应关系也要提前确认否则启动时会出现奇怪的兼容性报错。如果实训用的是Spring Boot 3.x就必须用JDK 17以上如果是Spring Boot 2.7JDK 8和JDK 11都可以跑。项目结构我建议一开始就规范化不要随手把所有类都堆在一个包底下com.example.library ├── controller # 控制器层 ├── service # 业务层接口 ├── service.impl # 业务实现 ├── mapper # MyBatis的Mapper接口 ├── entity # 实体类 ├── dto # 参数接收对象 ├── config # 拦截器、全局配置 └── common # 统一返回结果、异常处理这里有一个小习惯值得从一开始就养成Controller里不要直接写业务代码也不要直接把Service层的结果塞成Map返回。定义一个统一的Result对象无论接口调用成功还是失败都返回一致的结构。后面写前端、写测试用例都会轻松很多。public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ... } public static T ResultT error(String message) { ... } }别看这东西小它能让整个项目的前后端交互风格统一也能避免后期接口改了字段导致前端到处都是兼容性判断。3. 核心功能开发实录从登录到借阅的完整链路3.1 登录与权限加密、会话、拦截器一个不能少登录模块几乎是每个实训项目都必须有的功能但也是问题重灾区。很多同学的实现方式是前端传用户名和密码后端查一下库对得上就放行对不上就提示失败。看着没问题实际上一身漏洞。密码绝对不能明文存数据库。实训阶段不用引入整套Spring Security只引入它的加密工具类就能满足需求dependency groupIdorg.springframework.security/groupId artifactIdspring-security-crypto/artifactId /dependency注册时对密码进行BCrypt加密登录时用matches校验BCryptPasswordEncoder encoder new BCryptPasswordEncoder(); // 注册 String safePassword encoder.encode(rawPassword); user.setPassword(safePassword); // 登录校验 boolean matched encoder.matches(rawPassword, user.getPassword());BCrypt是自带盐值的哈希算法同一个密码每次加密出来的结果都不同但matches方法可以正确比对。这比MD5加盐还要省心是目前最主流的密码存储方式。登录成功后我会建议用Session保存登录用户的信息同时配合拦截器做访问控制。Spring Boot里实现这种方式非常顺手public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object user request.getSession().getAttribute(loginUser); if (user null) { response.sendRedirect(/login); return false; } return true; } }再注册拦截器并指定放行路径Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/login, /register, /css/**, /js/**); } }这一步的价值在于你用最小的代价理解了“会话管理”和“访问控制”。很多真实项目的权限体系无论多复杂底层思路和这个拦截器是一样的。3.2 图书检索与分页这条老路为什么一直有人踩坑图书列表和按关键词检索看起来简单但做起来有几个非常经典的坑。第一个坑是SQL拼接时直接使用字符串拼接导致注入风险。正确写法是使用MyBatis的#{}而不是${}。模糊查询时要注意like条件MySQL里要写成CONCAT(%, #{keyword}, %)如果直接传%s进去可能什么都查不到。select idsearchBooks resultTypecom.example.library.entity.Book SELECT * FROM book where if testkeyword ! null and keyword ! AND (title LIKE CONCAT(%, #{keyword}, %) OR author LIKE CONCAT(%, #{keyword}, %) OR isbn LIKE CONCAT(%, #{keyword}, %)) /if AND status 1 /where ORDER BY id DESC LIMIT #{offset}, #{pageSize} /select第二个坑是分页参数。很多新手会犯“前端传pageSize后端直接用页码乘以每页条数”但边界没处理好的问题。建议在Service层做一个统一的分页参数校验页码从1开始每页数量限制在1到100之间。逻辑虽然简单却能避免很多因为前端传了0或负数导致的异常。第三个坑发生率不低分页查询时如果用户修改了查询条件前后页码计算就会错乱。所以在页面里要把keyword一起传回给查询条件并保证翻页链接里带上原始关键词。这个细节不处理好用户会感觉“明明搜了某本书翻到第二页结果全是无关数据”。3.3 借还书业务事务和并发是进阶分水岭图书借阅的核心操作是先检查库存是否大于0再把available_count减1最后插入一条借阅记录。这三个操作缺一不可如果中间任何一步失败都会导致数据不一致。实训里很多同学写出这样的逻辑看着没毛病但实际一跑就出问题两个读者同时借同一本只剩一本库存的书系统可能都检查到available_count为1然后同时允许借阅最后库存被减成了-1。解决并发问题可以从两个层面入手。第一个层面是数据库层面执行更新时加上条件“库存大于0”UPDATE book SET available_count available_count - 1 WHERE id #{bookId} AND available_count 0;如果影响行数为0说明这本书已经被借完了直接提示“库存不足”。这个写法利用了数据库行锁本质上比先查再更新更安全。第二个层面是事务。Spring里加一个Transactional注解就能保证借书、扣库存、插记录要么全部成功要么全部回滚Transactional(rollbackFor Exception.class) public void borrowBook(Long userId, Long bookId) { Book book bookMapper.selectByIdForUpdate(bookId); if (book null || book.getAvailableCount() 0) { throw new BusinessException(库存不足); } bookMapper.decreaseAvailable(bookId); borrowRecordMapper.insert(new BorrowRecord(userId, bookId)); }借书时还要生成due_time也就是应还日期一般可以设为借出时间加30天并提醒用户超过这个日期就算逾期。还书的时候逻辑相对简单更新借阅记录状态、归还时间再把图书库存加回去。这里我想强调一个经验以后去真实公司做业务事务边界怎么划、锁怎么控制是面试的高频考点。实训阶段哪怕实现的方案比较简单也一定要在文档和复盘里写清楚“这里存在并发问题我是怎么处理的”这会让你和其他只会写CRUD的同学拉开很大差距。3.4 前端交互不求炫技但求能用实训项目的前端不需要特别花哨但有几个基础体验必须保证。列表页面最好有搜索框、分页条、操作按钮。表单页面要做简单的非空校验。Bootstrap默认样式虽然不够精致但胜在稳定不会出现自写CSS在不同浏览器下错乱的问题。我做实训时会要求至少做到这几点所有操作按钮有明确反馈要么跳转成功页要么弹出结果提示。表单校验在前后端都做一遍前端用于体验后端用于安全。页面上的时间、状态、库存等字段由后端格式化后输出前端不要做复杂计算。前端做好了整个项目才像“一个系统”否则只能算“一套接口的集合”。很多学生总觉得自己技术牛不愿意在页面上下功夫最后答辩时界面简陋反而把后端亮点盖住了不值得。4. 测试、部署与排查实训的最后一道关卡4.1 功能测试按用户操作路径列清单实训走到收尾阶段最难受的就是“平时跑得好好的一演示就崩”。解决这个问题没有捷径就是提前按用户操作路径做一遍完整的冒烟测试。我说的测试不是随便点点鼠标而是列成一个清单逐项打勾。建议至少覆盖这些场景操作路径预期结果重点关注注册新用户注册成功并自动登录密码是否加密存储使用错误密码登录登录失败且提示清晰错误信息是否友好查询不存在的书名列表为空页面不报错空数据处理借一本库存为0的书提示库存不足并发边界正常借书库存减1生成借阅记录事务完整性正常还书状态变为已还库存加1状态变更管理员删除被借出的书系统拦截并提示外键/业务约束每测出一条Bug不要急着改完就完事把复现场景和日志记下来这就是后面复盘的第一手素材。4.2 从本地到服务器的部署过程实训项目要能真正跑起来部署环节绕不开。我建议统一用“打包成可执行jar”的方式简单直接也符合现在Spring Boot项目的主流部署思路。本地打包mvn clean package -DskipTests打包完成之后在target目录下会生成一个可运行jar部署机器只要装了JDK就能启动java -jar library-system.jar --spring.profiles.activeprod如果希望项目宕机后自动重启在Linux服务器上可以写一个systemd服务[Unit] DescriptionLibrary System Afternetwork.target [Service] ExecStart/usr/bin/java -jar /opt/library/library-system.jar Restartalways RestartSec10 Userdeploy [Install] WantedBymulti-user.target部署中最容易忽略的是配置文件区分。开发环境数据库连接和服务器上肯定不一样我习惯用application-dev.yml和application-prod.yml两个配置并用spring.profiles.active切换。这样本地跑和服务器跑互不干扰。4.3 实训期间最常见的5个问题与排查思路第一个问题是“Forbidden 403”。原因大多是登录拦截器没有放过静态资源或者Session过期。排查思路是先看是否登录再看拦截器放行路径。第二个问题是“数据库连接失败”。一般来说是账号密码、IP端口配置写错了。实训环境里最常见的其实是MySQL未启动或者密码里带了特殊字符却忘了加转义。第三个问题是“中文乱码”。数据库连接URL要显式加上characterEncodingutf8mb4页面编码统一UTF-8。这些都是老问题解决方案都很固定。第四个问题是“前端提交的数据无法绑定到后端实体”。这通常是因为表单字段名和实体类属性名不一致或者没有提供setter方法。排查时直接看后端日志里的参数绑定报错。第五个问题是“打包后运行报找不到主类”。绝大多数是maven插件配置缺失需要检查pom里是否配置了spring-boot-maven-plugin。这些问题的共同特点是错误信息已经告诉了你答案但新手容易慌。我一直强调遇到报错先看最下面三行不要被大段堆栈吓到大部分问题都是配置类问题和算法水平无关。5. 个人实训记录与复盘写日志比写代码更值钱5.1 好用且不费时的记录模板很多同学实训结束写总结时发现自己什么都想不起来就是因为过程记录做得不够。我在实训期间会强制自己每天用很少的时间做记录不需要大段文字按模板填空就行。每天用这个模板记录今天的任务目标是什么。实际完成了什么和计划有什么差异。遇到了哪些报错或卡点原因是啥。哪些代码或设计是自己觉得比较满意的。明天准备做哪几件事。问题日志用另一个模板问题描述在哪个页面、做了什么操作出现什么现象。环境信息操作系统、浏览器、JDK版本等。排查过程先看了哪条日志做了哪些尝试。最终解决改了什么配置/代码。这个坑给我什么启发。这些记录看着琐碎但到写实训报告、答辩准备、甚至以后找工作时整理项目经历都是非常宝贵的素材。好记性不如烂笔头这句话在编程领域是真的。5.2 复盘下次实训我一定会提前做的10件事实训结束后的复盘比实训本身更有价值。我带每一期班都会根据个人记录整理一张“下次必做清单”这次也一并分享出来第一天就统一好JDK、Maven、MySQL版本不做版本混战。数据库表设计必须通过文档评审再动手建表。所有接口路径和返回结果结构提前约定好禁止边写边改。拿到需求后先列测试清单再写代码。每个功能做完立刻提交一次代码不要攒到最后一起提交。遇到问题先搜日志搜不到再问别人。借书、还书这类涉及多步操作的逻辑必须放在Service层并加事务。密码和支付相关功能是底线不能存明文不能用简单加密糊弄。项目打包部署要提前演练一次不要留到答辩前一天才做。记录每天的进度和问题别把复盘留到最后一晚。把这条清单放在这篇记录的结尾是因为我觉得它比任何单一知识点都更通用。技术会在几年内更新换代但这些实训中养成的习惯和意识会带到以后每一个真实项目里。我自己的体会是实训周期虽然不长但它逼着你走完一个项目从需求到部署的全过程这本身就是成本最低的项目管理训练。只要你愿意记录、愿意复盘哪怕项目小而简单你收获的东西也绝不会小。