软件工程实战:从需求到测试的四大关键
摘要: 本文系统复盘了《软件工程实务》课程中的四个高价值认知:需求分析如何用 UML 用例图 拒绝伪需求、架构设计时 MVC 解耦 的落地方案、编码阶段 Git分支管理 的血泪教训,以及测试阶段 JUnit 自动化测试 的脚本实现。文末附赠组员评分表和避坑清单。
引言:大学里最特殊的一门课
如果把大二的程序设计比作“造一个可以动的玩具”,那《软件工程实务》就是“盖一栋需要住人的楼”。
学期初,老师把全班分成若干五人小组,要求在9周内交付一套Web系统。我自荐当了组长。回头看,代码量总计约8000行,而文档加起来超过了1.5万字。正是这1.5万字,让我真正明白了软件工程的骨架究竟是什么。
第一章:需求分析不是记笔记,是签合同
1.1 第一次翻车
我们选的项目是“基于Spring Boot的线上二手书交易平台”。我最初写的需求文档长这样:

错误示范:
功能1:用户注册登录
功能2:浏览商品
功能3:下单购买
老师看完反问了我三句话,我当场哑口无言:
-
卖家怎么上架旧书?ISDN扫描识别还是手动录入?
-
“下单”之后钱给谁了?你们做不做支付,还是货到付款?
-
书卖掉了,库存状态怎么同步?
这一刻我意识到,老板要的功能逻辑,和用户遇到的实际场景,中间差着十万八千里
1.2 重构:用用例图定位边界
我们花了一整个周末重做需求调研,画出了核心用例图:
graph TD
A[普通用户]
B[卖家]
C[系统管理员]
A --- A1((浏览/搜索图书))
A --- A2((加入购物车))
A --- A3((下订单))
B --- B1((发布图书信息))
B --- B2((确认发货))
B --- B3((查看销售记录))
C --- C1((审核图书上架))
C --- C2((处理投诉))
C --- C3((用户管理))

在此基础上,我们把用例描述细化到触控级别:
| 用例ID | 用例名称 | 参与者 | 前置条件 | 基本事件流 |
|---|---|---|---|---|
| UC-01 | 发布图书 | 卖家 | 已登录 | 1.点击“我要卖书” 2.扫描/输入ISBN 3.系统调用豆瓣API自动填充书名、封面 4.卖家填写新旧程度、价格 5.提交进入待审核 |
| UC-02 | 下单购买 | 买家 | 购物车非空 | 1.确认购物车 2.填写收货地址 3.系统生成待付款订单 4.跳转模拟支付页面 5.支付成功扣减库存 |
1.3 一课一得 ①:需求是一切灾难的源头
核心认知:需求阶段的1小时修改,等于编码阶段的10小时重写。
这个阶段产出的SRS(软件需求规格说明书)被老师评为班级第一,原因只有一条:我们把所有功能都还原成了“用户在什么情况下、用这个功能、达成什么目的”的三段式描述。
第二章:设计——从一张E-R图开始的软件骨架
2.1 为什么不能直接开写?
组员小刘是个急性子,在文档还没写完时就搭建了一个Spring Boot空项目,把数据库建了出来。结果表里连“订单状态”这个基础字段都没有,因为当初我们压根没考虑到“待支付、已支付、已发货、已收货、已取消”这五种状态的流转。
这件事让我们组付出了惨痛代价:第一版物理数据库被全部删掉重建。
2.2 E-R图与表结构设计
我们重新设计了完整的E-R图(转换为关系模式):

核心实体(5表):
-
user (用户表)
-
idBIGINT PK AUTO_INCREMENT -
usernameVARCHAR(50) NOT NULL UNIQUE -
password_hashCHAR(60) NOT NULL -
phoneVARCHAR(20) -
roleENUM('BUYER','SELLER','ADMIN') NOT NULL -
create_timeDATETIME DEFAULT CURRENT_TIMESTAMP
-
-
book (图书表)
-
idBIGINT PK -
isbnVARCHAR(13) -
titleVARCHAR(200) NOT NULL -
authorVARCHAR(100) -
cover_urlVARCHAR(500) -
owner_idBIGINT FK -> user.id -
original_priceDECIMAL(10,2) -
selling_priceDECIMAL(10,2) NOT NULL -
condition_descTEXT -
statusENUM('PENDING','APPROVED','REJECTED','SOLD') DEFAULT 'PENDING'
-
-
order_main (订单主表)
-
idBIGINT PK -
order_noVARCHAR(32) UNIQUE NOT NULL -
buyer_idBIGINT FK -> user.id -
total_amountDECIMAL(10,2) NOT NULL -
order_statusENUM('UNPAID','PAID','SHIPPED','RECEIVED','CANCELLED') DEFAULT 'UNPAID' -
create_timeDATETIME -
pay_timeDATETIME
-
-
order_item (订单明细)
-
idBIGINT PK -
order_idBIGINT FK -> order_main.id -
book_idBIGINT FK -> book.id -
purchase_priceDECIMAL(10,2)
-
-
system_log (操作日志)
-
idBIGINT PK -
user_idBIGINT -
operationVARCHAR(100) -
ip_addressVARCHAR(45) -
op_timeDATETIME
-
同时,我们写了一版详细的API接口文档片段:
/**
* 图书服务接口 - 概要设计产出物
*/
@RestController
@RequestMapping("/api/v1/books")
public class BookController {
@Autowired
private BookService bookService;
/**
* 分页查询已上架图书
* 请求方式: GET
* 请求路径: /api/v1/books?page=1&size=20&keyword=算法
*/
@GetMapping
public ApiResponse<PageResult<BookVO>> listBooks(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "20") Integer size,
@RequestParam(required = false) String keyword
) {
return ApiResponse.success(bookService.findAllByPage(page, size, keyword));
}
}

2.3 一课一得 ②:设计是团队协作的共同语言
核心认知:代码还没写一行,但所有接口签名、字段类型、状态枚举值已经全部定死——这就是设计的力量。
这份设计文档后来成了组内的“字典”。谁对字段有疑问,第一反应是翻这篇文档而不是直接问,效率提升一倍。
第三章:编码实战——Git分支管理的真实教训
3.1 合并地狱
我永远记得项目第四周的周三晚上10点。小刘和小张同时往main分支合并代码,Git报了22个冲突。我们三个人在腾讯会议屏幕共享,从10点手动消冲突消到了凌晨1点40。
第二天我把Git规范写进了组规:
# ========== 我们的Git分支规范 ==========
# 1. 主分支保护
git checkout main
# 禁止直接push到main,只接受Pull Request合并
# 2. 功能分支命名规则
git checkout -b feature/书籍搜索功能
git checkout -b feature/订单支付模块
# 3. 修复分支命名规则
git checkout -b bugfix/购物车数量计算错误
git checkout -b hotfix/数据库连接池溢出
# 4. 每天下班前必须执行的操作顺序
git add .
git commit -m "[feat] 完成卖家发布图书的后端接口 #UC-01"
git pull origin main --rebase
# 解决完本地的rebase冲突后
git push origin feature/书籍搜索功能
同时我们把 .gitignore 文件完善了三版,确保 .idea/ 、 *.log 、 application-prod.yml 这些敏感文件永远不会被提交。
3.2 一课一得 ③:Git规范不是条条框框,是团队的保险丝
核心认知:在没有规范的自由里,五个人能在24小时内毁掉整个仓库。
第四章:测试——找到那个隐藏最深的Bug
4.1 场景描述
系统上线前三天,我发现一个诡异现象:当两个人同时下单购买同一本书时,两个人的订单都显示“已付款”,但库存只扣了1个。这意味着在并发场景下,书会被“卖两次”。
这是一个典型的超卖Bug,在高并发下才会触发。
4.2 测试脚本与修复
我立刻补了一个JUnit并发测试用例:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
@SpringBootTest
public class OrderConcurrencyTest {
@Autowired
private OrderService orderService;
@Autowired
private BookService bookService;
@Test
public void testConcurrentOrderPreventOverselling() throws Exception {
int threadCount = 20; // 模拟20人同时抢一本书
Long bookId = 10001L;
// 初始化库存为1本
bookService.setStock(bookId, 1);
ExecutorService pool = Executors.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch(threadCount);
AtomicInteger successCount = new AtomicInteger(0);
for (int i = 0; i < threadCount; i++) {
pool.execute(() -> {
try {
Long userId = Thread.currentThread().getId();
boolean result = orderService.createOrder(userId, bookId);
if (result) {
successCount.incrementAndGet();
}
} finally {
latch.countDown();
}
});
}
latch.await();
pool.shutdown();
// 最终库存应为0,成功下单数必须为1
int finalStock = bookService.getStock(bookId);
System.out.println("最终库存: " + finalStock);
System.out.println("成功下单数: " + successCount.get());
assertEquals(0, finalStock, "库存必须为0,防止超卖");
assertEquals(1, successCount.get(), "20个人抢1本书,只能有1个成功");
}
}
修复方案: 在下单的Service层加了数据库行级锁:
-- 使用SELECT ... FOR UPDATE 防止并发超卖
SELECT stock FROM book WHERE id = #{bookId} FOR UPDATE;
4.3 一课一得 ④:软件质量不靠人眼保证,靠测试用例堆出来
核心认知:并发Bug就像隐形炸弹,没测到就等于没解决。
第五章:项目交付的最后三公里
5.1 写部署文档的觉悟
系统部署到老师的验收环境时,出了一件尴尬的事:我们本地用的MySQL 8.0,老师的服务器装的是MySQL 5.7。建表语句里的 utf8mb4 字符集、新日期类型的默认值语法,在5.7上报了整整17个Error。
我们连夜赶出一份《部署环境依赖性清单》,连中间件版本号都精确到了小版本:
| 软件 | 版本要求 | 备注 |
|---|---|---|
| JDK | 11.0.19 | 不支持JDK8以下 |
| MySQL | 8.0.33 | 最低8.0,需开启InnoDB引擎 |
| Redis | 7.0.11 | 用于存储登录态Token和购物车临时数据 |
| Nginx | 1.24.0 | 应配置反向代理到8080端口 |
5.2 一课一得 ⑤:真正交付的是“系统 + 文档 + 安全感”
核心认知:你的代码在别人环境里跑不起来,那就不是Bug,是交付失败。
第六章:组内评分表与个人成长复盘
6.1 组员贡献度互评
老师要求我们提交一份组内互评表。我们小组第一次打分时吵得面红耳赤,后来定下量化标准才顺利通过:
| 成员 | 角色 | 代码得分 | 文档得分 | 协作沟通 | 总分 |
|---|---|---|---|---|---|
| 张XX(我) | 组长/后端 | 90 | 95 | 92 | 92.3 |
| 刘XX | 前端主力 | 88 | 80 | 90 | 86.0 |
| 王XX | 数据库设计 | 85 | 92 | 85 | 87.3 |
| 李XX | 测试 | 82 | 88 | 88 | 86.0 |
| 赵XX | UI/部署 | 80 | 85 | 90 | 85.0 |
6.2 我的五个一课一得
| 阶段 | 核心认知提炼 |
|---|---|
| 需求分析 | SRS文档要把用户故事穷举到不会产生歧义的程度 |
| 概要设计 | 数据库ER图和接口文档是团队协作的唯一标准 |
| 编码 | Git规范是项目保命的底线原则 |
| 测试 | 自动化测试是代码防塌方的最后一道安全网 |
| 交付 | 环境不一致不属于技术Bug,属于部署事故 |

写在最后:软件工程是一门遗憾的艺术
如果让我重来一次,我会在第二周就引入自动化测试脚本,而不是第四周出了合并事故才补救。我会在第一天就定下接口文档的更新规则,而不是等前端跑来问我“这个字段改类型了你为什么不通知我”。
软件工程实务,消除的不是代码里的Bug,而是团队协作里的信息差。
感谢这门课程的五次小组评审,每一次都像给项目做了一次CT扫描。更感谢组员们在凌晨的腾讯会议里没有挂机。
代码会过时,但工程化的肌肉记忆不会。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)