简介:《软件工程课程设计指导书》是面向计算机科学与技术专业学生与软件工程初学者的课程设计实践文档,由周兵编写,用于支撑 10 学时的上机实践,帮助读者按照软件工程思想与面向对象方法完成一个小型软件系统。内容以选课系统为完整案例,串联需求分析、软件设计、软件测试与源代码编写,涉及 C++ Builder 6.0 与 Access 2000 环境,说明了用例图、序列图、对象模型、状态图等文档的撰写要求,附录还包含 ODBC 数据源建立、C++ Builder 上手与数据库连接的操作指引。压缩包内仅 1 个 doc 文件,约 539KB,体积轻便,便于打印或随课程进度查阅。目前已有 99 人学习。读者可据此明确设计题目、字段与功能清单、报告结构与成绩考核方式,减少选题偏差和文档返工,也适合教师作为课程设计任务书模板参考。
1. 一份 10 学时的软件工程课程设计指导书,其实是一条从建库到交报告的流水线
第一次翻这份《软件工程课程设计指导书》,我以为又是那种满篇「需求分析的重要性」的理论册子。结果第 3 页就开始教人在 BDE Administrator 里右键建数据源,第 5 页已经在拖 ADOConnection 控件,最后几页直接甩出报告目录:用例图、序列图、对象模型、状态图、至少 20 组测试数据、只交手写源码。这份指导书真正的价值不是讲软件工程概念,而是把「需求分析 → 对象建模 → 建库连库 → 功能实现 → 测试设计 → 交报告」拧成一条 10 学时能跑完的流水线,落点是所有人都熟的选课系统。技术栈是 C++ Builder 6.0 加 Access 2000,老得掉牙,却老得刚好:没有框架帮你藏东西,每个环节都看得见摸得着。适合两类人,一类是要交课程设计的学生,照着走能少返工;一类是想补一遍完整开发流程的开发者,把它当成最小可复现的样板工程。
2. 技术栈与工程结构:C++ Builder 6.0 + Access 2000 这套组合怎么摆平
2.1 为什么是这套「老古董」而不是更现代的技术栈
指导书把实验环境钉死在单机 Windows、C++ Builder 6.0、Access 2000,很多人第一反应是过时。放到 10 学时、单人单机的约束下看,这个选择其实相当合理,理由有三条,值得说清楚,因为后面所有工程组织方式都从这个前提推导出来。
第一,Access 的 .mdb 是文件型数据库,不需要装服务、不需要配账号权限、不需要建实例,把文件拷进工程目录就能跑,10 学时里省掉的部署时间足够多写两个功能页面。第二,C++ Builder 的 VCL 组件面板里有现成的 ADO 页签,拖一个 TADOConnection、一个 TADOQuery、一个 TDBGrid,界面和数据就接上了,学生不用先花两天学 Web 框架的路由和模板。第三,先修课程写的是「一门计算机高级语言、C++ 语言、数据库系统概论」,选 C++ Builder 能让 C++ 语法和数据库 SQL 同时用上,不会出现「学了三个学期,课程设计里一个都用不到」的尴尬。
代价也很明确:这套工具链只能跑 32 位,字符串默认按 ANSI 处理,BDE 和 ODBC 两层配置叠在一起,任何一层配错都表现为「连不上」。我在别的项目里带新人时也遇到过类似情况,老工具链的坑不在代码,在环境。
| 对比项 | C++ Builder 6.0 + Access 2000 | 现在常见的替代方案 |
|---|---|---|
| 部署成本 | 拷贝 .mdb + 配一个 ODBC 数据源 | 起服务、建库、配账号 |
| 代码可见性 | 全部手写在 Unit 里,无框架黑箱 | 框架分层多,初学者容易只改配置 |
| 学习曲线 | 会 C++ 就能上手拖控件 | 需要先懂框架生命周期 |
| 中文支持 | ANSI 代码页,需显式处理 | 默认 UTF-8 |
| 适合场景 | 单人单机、10 学时、要交完整文档 | 团队协作、需要联网部署 |
如果你想原汁原味复现这个课程设计,就按老栈走;如果你只是想借它的流程练手,把 C++ Builder 换成任何带可视化界面的语言都行,但数据库建模、事务处理、测试用例设计这三块不能省,那才是指导书真正想训练的部分。
2.2 指导书对应的工程目录与文件清单
指导书正文没给目录,但从它的操作步骤能反推出一个最小工程该长什么样。C++ Builder 6.0 的项目文件是 .bpr,窗体单元是 .cpp + .h + .dfm 三件套,数据库文件独立放在数据目录里,ODBC 数据源指向绝对路径。我一般会按下面这个结构建工程,一是让 .mdb 跟源码分开放,二是编译产物不污染提交目录。
XuanKe/ ├── XuanKe.bpr # 工程文件,记录所有 Unit 和编译选项 ├── XuanKe.cpp # 程序入口,Application->Run() ├── UnitMain.cpp / .h / .dfm # 主窗体:登录 + 功能导航 ├── UnitSelect.cpp / .h / .dfm # 选课界面 ├── UnitScore.cpp / .h / .dfm # 成绩查询界面 ├── UnitData.cpp / .h # 数据模块:全局 ADOConnection 封装 ├── data/ │ └── xk.mdb # Access 2000 数据库文件 ├── doc/ # 报告、用例图、序列图、状态图 └── test/ └── cases.csv # 20 组测试数据的落盘记录DFM 文件是窗体布局的二进制文本描述,控件位置、属性、事件绑定都记在里面,所以改名、挪控件最好在 IDE 里做,手改 DFM 很容易让工程打不开。UnitData 单独拆出来是我加的习惯,指导书里是直接在 Form1 上放 ADOConnection,功能一多,每个窗体各连一次数据库,连接数上去以后锁表现象会非常难查。
2.3 环境落地:把 2000 年代的工具链在今天跑起来
真正的第一道坎不是写代码,是让连接测试通过。指导书附录里的步骤是对的,但它默认你在一台全新的、32 位的、装好 BDE 的机器上操作。今天的机器大多 64 位,默认的 ODBC 管理器也是 64 位版本,打开以后根本找不到 Microsoft Access Driver (*.mdb),很多人卡在这一步就放弃了。
:: 32 位 ODBC 数据源管理器,老工具链必须从这里进 %SystemRoot%\SysWOW64\odbcad32.exe :: 64 位版本,只能看到 64 位驱动,对 C++ Builder 6 编译出的程序无效 %SystemRoot%\System32\odbcad32.exe :: BDE 管理器,建别名、Append To SQL Links 都在这里 "C:\Program Files (x86)\Borland\BDE\BDEADMIN.EXE"两个路径的区别是 32 位程序和 64 位驱动的隔离机制造成的,C++ Builder 6.0 编译出来的 exe 是 32 位进程,只能加载 32 位的 ODBC 驱动,所以数据源必须在 SysWOW64 那个入口里建。建完数据源以后回到 BDE Administrator,右键 Databases 新建一个别名,Driver Name 选 Microsoft Access Driver (*.mdb),然后在 Definition 页把 ODBC DSN 填成刚才建的名称,右键 Apply,字体变黑说明打通了。
还有一个细节:别把工程放在带空格的路径下。BDE 早期版本解析别名路径时对空格处理不稳,路径里带个空格就变成「数据源存在但打不开」,排查起来非常费时间。装一个干净的虚拟机,把工具链、Access、BDE 全装进去,做完快照,后面所有实验都在快照上跑,比在真机上反复折腾快得多。
3. 数据模型拆解:一张表撑不起选课系统,字段怎么归到 5 个实体
3.1 从字段清单到实体关系:指导书列的 26 个字段要拆开看
指导书要求「所涉及的字段至少应包括」并给了一长串,从学生姓名一直到总评成绩。新手最常见的做法是建一张大表,把这些字段全塞进去,然后发现一条选课记录里重复存了十几次学生信息和教师信息,改一个电话号码要更新几十行。这一步其实就是软件工程里讲的实体识别,只是指导书没明说怎么分。
我的拆法是以「谁在描述谁」为标准:描述人的归人,描述课的归课,描述「这门课在这个学期由谁开」的归教学班,描述「谁选了什么课、考了多少分」的归选课记录。四个实体加一个关系表,一共五张表,正好覆盖全部字段。
| 原始字段 | 归入表 | 处理说明 |
|---|---|---|
| 学生姓名、学号、登陆密码、性别、出生年月、籍贯、学生电话、地址、家庭地址 | 学生 | 「地址」与「家庭地址」是两个概念,前者按联系地址处理,拆成两个字段 |
| 教师号、教师姓名、教研室、职称、性别、教师电话 | 教师 | 性别字段在两张表里同名,建表时要避免混用 |
| 课名、课号、学分、先行课号、课时 | 课程 | 先行课号是自引用外键,指向课程表自己 |
| 课号、教师号、开课教室、人数限制、选课人数 | 开课 | 同一门课可以开多个班,所以主键是开课号而非课号 |
| 学号、课号、考试成绩、平时成绩、总评成绩 | 选课 | 主键是学号 + 课号的联合主键,天然挡住重复选课 |
这里有两个容易被忽略的点。先行课号是课程表对自身的外键,用图形化建模时那根箭头会绕回同一个矩形,画不出来就说明关系没想清楚。总评成绩不必单独存储,它由考试成绩和平时成绩按比例算出,存进去就有出现不一致的风险,常见做法是查询时现算,或者用一个计算字段。
3.2 建表 SQL 与主外键:把文档里的对象模型落到 Access
Access 2000 默认是图形化建表,但指导书要求报告里交对象模型,模型和实际表结构必须对得上。我习惯先写 DDL 再用 ADO 执行,这样表结构可复现,换个环境重新建库不用再点一遍鼠标。
-- Jet 4.0 方言:文本类型必须带长度,主键在字段级或表级都可以声明 CREATE TABLE 学生 ( 学号 TEXT(10) NOT NULL, 姓名 TEXT(20) NOT NULL, 密码 TEXT(20) NOT NULL, 性别 TEXT(2), 出生年月 DATETIME, 籍贯 TEXT(30), 联系地址 TEXT(50), 家庭住址 TEXT(50), 学生电话 TEXT(20), CONSTRAINT PK_学生 PRIMARY KEY (学号) ); CREATE TABLE 课程 ( 课号 TEXT(8) NOT NULL, 课名 TEXT(40) NOT NULL, 学分 INTEGER, 课时 INTEGER, 先行课号 TEXT(8), -- 自引用外键,可为空 CONSTRAINT PK_课程 PRIMARY KEY (课号), CONSTRAINT FK_课程_先行 FOREIGN KEY (先行课号) REFERENCES 课程(课号) ); CREATE TABLE 选课 ( 学号 TEXT(10) NOT NULL, 课号 TEXT(8) NOT NULL, -- 这里存的是开课号 考试成绩 INTEGER, 平时成绩 INTEGER, CONSTRAINT PK_选课 PRIMARY KEY (学号, 课号), CONSTRAINT FK_选课_学生 FOREIGN KEY (学号) REFERENCES 学生(学号), CONSTRAINT FK_选课_开课 FOREIGN KEY (课号) REFERENCES 开课(开课号) );主键选联合主键是关键一步,学号加开课号的组合唯一,重复选课会在数据库层被直接拒绝,不用在代码里先查一遍再插。外键声明在 Jet 里生效,删学生记录时如果有选课记录会被拦住,这个约束在调试阶段很烦,但它能防止程序 bug 把成绩数据变成孤儿行。索引方面,选课表按学号、开课表按课号各建一个普通索引,查询界面翻页时会明显快一截。
3.3 选课人数与人数限制:一个必须用事务兜住的字段
指导书的功能列表里,选课和退课各占一条,但没提人数限制怎么处理。这是整个系统里唯一有并发语义的地方,也是报告里最容易写不出东西的地方。教学班有人数限制,选课人数是实时统计值,如果代码写成「先查还有没有名额,再插一条记录,再更新计数」,两个操作之间有窗口,人数超限几乎是必然的,虽然课程设计是单机,但教师端批量导入和学生在另一台机器上同时操作的情况,助教检查时一测就露。
正确的做法是把「占名额」做成一条带条件的原子更新,条件不成立就说明满了,靠返回值判断而不是先查后改。
// UnitSelect.cpp —— 选课:占名额与写记录必须在同一个事务里 bool TFormSelect::DoSelect(const AnsiString &sid, const AnsiString &openId) { TADOQuery *q = new TADOQuery(NULL); q->Connection = DM->Conn; // 全局数据模块里的连接 try { DM->Conn->BeginTrans(); // 开启事务 // 第一步:只有还有余量时才加一,条件写在 WHERE 里保证原子性 q->SQL->Text = "UPDATE 开课 SET 选课人数 = 选课人数 + 1 " "WHERE 开课号 = :oid AND 选课人数 < 人数限制"; q->Parameters->ParamByName("oid")->Value = openId; q->ExecSQL(); if (q->RowsAffected != 1) { // 0 行受影响 = 满员或课号不存在 DM->Conn->RollbackTrans(); return false; } // 第二步:写入选课记录,重复选课会被联合主键拒绝 q->SQL->Text = "INSERT INTO 选课(学号, 课号) VALUES(:sid, :oid)"; q->Parameters->ParamByName("sid")->Value = sid; q->Parameters->ParamByName("oid")->Value = openId; q->ExecSQL(); DM->Conn->CommitTrans(); // 两步都成功才提交 return true; } catch (Exception &e) { DM->Conn->RollbackTrans(); // 任何异常都回滚,计数不会虚增 ShowMessage("选课失败:" + e.Message); return false; } }三个参数要盯住:Conn 是全局唯一连接,事务是在连接级别生效的,所以同一窗体里千万别另开第二个 TADOConnection;RowsAffected 取的是受影响行数,这条 UPDATE 自然只可能影响 0 或 1 行,判等条件很干净;开课号存进选课表的课号字段,是因为同一门课分多个班,退课和成绩都必须按班区分。退课是镜像操作,先删记录再减一,顺序反了会出现「计数减了但记录还在」的脏数据。
4. 核心功能实现:登录、选课、退课、成绩查询的代码骨架
4.1 数据模块:TADOConnection 的连接串怎么写才不飘
指导书附录第三步讲的是拖一个 ADOConnection 然后点 Build 按钮向导生成连接串,这条路能通,但生成出来的字符串是一长串 Provider 参数,换台机器就失效。我在 UnitData 里做了一层封装,把连接串写死成引用 ODBC 数据源名的形式,只要机器上建了同名 DSN,工程拷到哪里都能用。
// UnitData.cpp —— 全局数据模块,所有窗体共用一条连接 TADOConnection* TDM::Conn = NULL; TADOConnection* TDM::GetConn() { if (Conn == NULL) { Conn = new TADOConnection(NULL); Conn->LoginPrompt = false; // 关掉每次弹出的登录框 Conn->ConnectionTimeout = 5; // 连不上时 5 秒超时,不干等 Conn->ConnectionString = "Provider=MSDASQL.1;Persist Security Info=False;" "Data Source=选课数据源"; // 与 ODBC 管理器里的名字逐字一致 try { Conn->Connected = true; } catch (Exception &e) { ShowMessage("数据源连接失败,请检查 ODBC 是否建好:" + e.Message); } } return Conn; }Provider=MSDASQL.1 是微软的 OLE DB 到 ODBC 的桥接层,走它才能用上 ODBC 里建好的 DSN;Data Source 后面跟的是数据源名,不是 .mdb 文件路径,这是最常见的混淆点,写成文件路径一样能连,但换目录就断;LoginPrompt 设 false 是为了避免每次操作弹一次框,Access 数据库本来就没账号密码,弹出来纯属浪费时间。
4.2 登录:密码校验和角色分流写在一次查询里
指导书的字段清单里只有一个登陆密码,没写权限区分,但注册的学生和开课的老师显然是两类人。我把登录做成先查学生表、查不到再查教师表,命中后把角色和 ID 存到全局变量里,后续窗体按角色决定显示哪些按钮。
// UnitMain.cpp —— 登录,返回角色:0 失败,1 学生,2 教师 int TFormMain::DoLogin(const AnsiString &account, const AnsiString &pwd) { TADOQuery *q = new TADOQuery(NULL); q->Connection = DM->GetConn(); q->SQL->Text = "SELECT 学号, 姓名 FROM 学生 WHERE 学号 = :a AND 密码 = :p"; q->Parameters->ParamByName("a")->Value = account; q->Parameters->ParamByName("p")->Value = pwd; q->Open(); if (!q->Eof) { // 学生命中 gUserID = q->FieldByName("学号")->AsString; gUserName = q->FieldByName("姓名")->AsString; q->Close(); return 1; } q->Close(); q->SQL->Text = "SELECT 教师号, 教师姓名 FROM 教师 WHERE 教师号 = :a AND 密码 = :p"; q->Parameters->ParamByName("a")->Value = account; q->Parameters->ParamByName("p")->Value = pwd; q->Open(); if (!q->Eof) { gUserID = q->FieldByName("教师号")->AsString; gUserName = q->FieldByName("教师姓名")->AsString; q->Close(); return 2; } q->Close(); return 0; }参数化查询是这段代码最重要的部分,用 :a 占位再赋值,账号框里输入什么都不会拼进 SQL 文本。课程设计报告里的安全章节经常没东西可写,把这一点写进去,需求、设计、测试三处都能对得上。密码明文存储在这个场景下可以接受,因为它就是个教学系统,但如果要二次开发成能演示的项目,必须先换成哈希,这一点放到最后一章说。
4.3 成绩录入与查询:总评成绩的算法该放在哪一层
指导书的字段里有考试成绩、平时成绩、总评成绩三个,如果三个都存进表,录入界面就得算一次,教师改平时成绩的时候还要记得重算总评,一忘就出不一致。我的做法是只存前两个,总评在 SQL 里现算,比例写成常量。
// UnitScore.cpp —— 按开课号查成绩,总评按 7:3 现算 void TFormScore::LoadScore(const AnsiString &openId) { TADOQuery *q = new TADOQuery(NULL); q->Connection = DM->GetConn(); q->SQL->Text = "SELECT s.学号, s.姓名, c.考试成绩, c.平时成绩, " " ROUND(c.考试成绩 * 0.7 + c.平时成绩 * 0.3, 0) AS 总评 " "FROM 选课 c INNER JOIN 学生 s ON s.学号 = c.学号 " "WHERE c.课号 = :oid " "ORDER BY c.学号"; q->Parameters->ParamByName("oid")->Value = openId; q->Open(); DBGrid1->DataSource->DataSet = q; // 直接挂到表格控件上 }ROUND 到整数是课程设计的简化处理,真实场景一般保留一位小数并按绩点换算。总评放查询层的好处是改比例只动一处,坏处是没法对总评建索引、不能按总评排序分页,数据量大了要换成物化字段加触发器。这个权衡本身就可以写进报告的设计章节,评审看的就是你有没有意识到取舍。
5. 避坑与排查:这套老工具链最容易翻车的五个地方
5.1 环境与连接类:数据源明明建了,程序还是说连不上
第一条,现象是 BDE 里测试连接字体不变黑,或者程序启动就弹「未找到数据源名称」。
原因通常有两个。一是位数不匹配,你在 64 位 ODBC 管理器里建了数据源,程序是 32 位进程,两边各看各的,互相不认识。二是 BDE 别名建了但没 Apply,配置停在内存里没落盘,重启后消失。
解决顺序固定下来:先从 SysWOW64 目录进 ODBC 管理器确认数据源存在且能看到 Access 驱动,再进 BDE 确认别名存在、ODBC DSN 那一栏填的和数据源名逐字一致,右键 Apply 之后再右键 Open 测一次。三条都过了还连不上,就去查 .mdb 文件是不是被 Access 独占打开了,文件锁也会表现为连不上。
第二条,现象是编译一切正常,运行时弹出「Cannot open database」,路径看着没问题。
原因多半是相对路径的参照点变了。C++ Builder 的 IDE 运行时工作目录是工程目录,双击 exe 运行时工作目录是 exe 所在目录,两者不同,用相对路径找 .mdb 就会一头能跑一头报错。
解决办法是把连接串里的数据源名跟路径解耦,永远通过 ODBC 数据源间接寻址,DSN 里存的是绝对路径,程序只认名字。另外在报告里把「ODBC 数据源名」写成部署步骤的一部分,换个机器第一件事就是建同名数据源,比改代码可靠。
5.2 数据与编码类:中文变成问号,字段长度也不够
第三条,现象是界面里录入的中文姓名,存进 Access 以后变成乱码或者问号,用 Access 直接打开又是好的。
原因是字符集在链路上被转了两次。C++ Builder 6.0 的 AnsiString 按系统代码页存储,简体中文环境下是 GBK;ODBC 驱动和 OLE DB 桥接层默认按 ANSI 传,Access 2000 的 .mdb 本身也是 ANSI 编码。三层只要有一层被当成别的代码页解释,中文就废了。
解决方式不是去改驱动,而是把数据库和系统代码页统一到 GBK,避免在中间任何一层做转换;字段类型用 TEXT 明确带长度,别用「备注」类型,备注字段在 ODBC 下读写的行为跟普通文本不一样,中文尤其容易出问题。建表和录入都在同一台中文系统上完成,中途别换语言环境。
第四条,现象是录入姓名超过十个字被截断,或者插入直接报字段太小。
原因是 Jet 的 TEXT(n) 里 n 是字符数,但 AnsiString 在某些控件里按字节截,一个汉字两字节,视觉上没超、实际已经超了。解决办法是界面输入框加 MaxLength 限制并同步到字段长度,建表时把中文类字段的长度按两倍预留,姓名给 20、地址给 50,比抠着算省心。
5.3 逻辑与交付类:人数超了、报告写不满 20 组用例
第五条,现象是检查时教师端显示某门课选了 31 人,人数限制写的是 30。
原因有两种,一种是选课代码写成了先查后改,中间没有事务,两次操作之间挤进了别的请求;另一种是退课只删了记录没减计数,或者减计数失败后没有回滚,日积月累把计数抬高。
解决方式前一章已经给过,把带条件的 UPDATE 和 INSERT 包在同一个事务里,异常一律回滚。另外建议在选课表上建联合主键,让重复选课在数据库层就被挡住,代码里少写一个「是否已选」的查询,逻辑简单了,出错面也小。定期跑一条核对 SQL,把选课人数和选课表的实际行数比一遍,对不上就说明某次操作漏了回滚。
第六条,不算坑,但很多人栽在这:报告要求至少 20 组不同的对象测试序列,学生写到第 8 组就编不下去了。
根子在用例是拍脑袋想出来的,没有从需求和边界推导。我的做法是按功能点分片,每个功能给正常流、异常流、边界值三类,登录 4 组、选课 5 组、退课 3 组、查询 4 组、成绩录入 4 组,刚好 20 组,每组写清输入、预期、实际三列。这套用例表存成 CSV 放进工程目录,后面改代码时按表回归一遍,比凭记忆点界面靠谱。
6. 二次开发:把课程设计改成能拿出来讲的项目
指导书给的这套东西跑通之后,最值钱的动作不是加功能,而是有方向地换掉它的短板。我给自己的改造路线分三步,每步都能单独验证,不会一次改崩。
第一步换数据库。把 Access 换成 SQLite 或轻量服务型数据库,连接层从 ODBC 改成原生驱动,这一步能验证你对数据访问层解耦得够不够干净。判断标准很简单:改完数据库,业务代码的 .cpp 文件修改行数应该少于十行。如果到处都在改,说明当初把 SQL 散落在各个窗体里了,先把查询收敛到数据模块再动手。
| 改造方向 | 原实现 | 建议替换 | 验证方式 |
|---|---|---|---|
| 数据存储 | Access 2000 .mdb | SQLite 或 MySQL | 换驱动后 20 组用例全过 |
| 密码存储 | 明文比对 | 加盐哈希 | 库里看不到明文,登录仍成功 |
| 并发控制 | 单机事务 | 数据库行锁或乐观版本号 | 两个客户端同时选最后一门课只成功一个 |
| 排课校验 | 无 | 同教室同时间冲突检测 | 制造冲突时提交被拒 |
第二步补业务规则。原系统只保证人数不超,没管同一学生选的两门课时间冲突、没管先修课没修过不能选后续课。这两条都能用一条 SQL 表达,写进选课前的校验里:时间冲突查开课表里时间字段是否重叠,先修课查该学生是否已有先行课号的通过记录。加规则的同时要同步补测试用例,20 组扩到 30 组,报告里那份测试表就立体了。
第三步提权改造。登录只做了一次身份判断,界面按钮全亮着,学生点得到教师端的成绩录入。常见做法是把角色判断收到菜单和按钮的 Enabled 属性上,再加一层服务端校验——单机版没有服务端,就在数据层每个写操作前再判一次角色,防止绕过界面直接调函数。
这里有个我踩过的习惯,从那以后我每次改完老的课程设计,都会先把 test/cases.csv 那张表导进一个临时脚本跑一遍,再手动点三个最关键的界面:登录失败、选最后一门课、退课后计数回落。手动三步花不到五分钟,能挡住改动引入的大部分回归。别嫌土,老工具链的编译反馈本来就慢,靠它兜底比事后翻日志划算。希望帮到你。
本文还有配套的精品资源,点击获取