news 2026/10/8 6:01:37

第一次作业_前后端分离计算器系统_中文版

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
第一次作业_前后端分离计算器系统_中文版

第一次作业 前后端分离计算器系统

学号:832401105
公网访问地址:http://43.128.135.64
提交状态:已确认完成

课程与作业信息

项目内容
课程软件工程
作业第一次作业 前后端分离计算器系统
作业目标完成一个由前端负责交互、后端负责表达式解析与计算、数据库负责持久化历史记录的计算器系统
核心要求四则运算、复合表达式、异常处理、历史记录查询与删除、前后端独立仓库、部署与博客说明
其他参考作业要求文档、前后端 README、前后端代码规范、系统总体设计文档 v2
截止时间2026 年 10 月 7 日 23:59

目录

  • Git 仓库与代码规范
  • PSP 表
  • 部署和评测说明
  • 成品展示
  • 需求分析
  • 总体架构
  • 项目结构
  • 前端设计
  • 后端设计
  • API 设计
  • 数据库设计
  • 表达式计算模块
  • 历史记录模块
  • 异常处理
  • 前后端交互流程
  • 测试
  • 关键代码说明
  • 开发过程与收获
  • 不足与后续改进
  • 总结

Git 仓库与代码规范

项目地址
前端仓库832401105_calculator_frontend
后端仓库832401105_calculator_backend
前端代码规范frontend codestyle.md
后端代码规范backend codestyle.md

前端和后端分别保存在两个 Git 仓库中。前端仓库只负责页面、状态和 HTTP 请求,后端仓库负责表达式处理、计算和 SQLite 数据访问。

PSP 表

预计时间来自项目中的832401105_Assignment1_PSP.xlsx,实际时间根据开发完成后的记录填写。预计总用时为 1015 分钟,目前已记录的实际用时为 1684 分钟;README 和 codestyle与“博客撰写”两项尚未提供实际时间。

PSP 阶段预计时间实际使用时间
需求分析10 min15 min
系统设计30 min104 min
Web 前端基础30 min78 min
前端界面开发60 min146 min
HTTP API JSON 学习60 min133 min
C++ 后端搭建120 min217 min
表达式解析模块75 min287 min
SQLite 学习与设计180 min96 min
计算历史模块75 min103 min
前后端联调120 min139 min
异常处理与完善60 min74 min
测试60 min88 min
部署30 min204 min
README 和 codestyle45 min20 min
博客撰写60 min30 min
合计1015 min1734 min

部署和评测说明

公网地址

公网访问地址:http://43.128.135.64

项目已经完成公网部署,助教可以通过上述地址访问并测试系统。以下本地运行说明仍保留,用于代码检查、独立构建和问题排查。

本地运行环境

  • 支持 C++17 的编译器;
  • CMake 3.16 或更高版本;
  • 任意静态文件服务器;
  • 现代浏览器;
  • 无需安装额外的 HTTP、JSON 或 SQLite 包,后端仓库已经包含相关源码。

后端启动

在后端仓库根目录执行:

cmake-Emake_directory data cmake-S.-Bbuild cmake--buildbuild

Windows 启动命令:

.\build\calculator_backend.exe

Linux 或 macOS 启动命令:

./build/calculator_backend

后端监听http://127.0.0.1:8080。程序必须从后端仓库根目录启动,使相对路径data/calculator.db能够正确解析。

前端启动

在前端仓库根目录启动静态文件服务器:

python-mhttp.server5500

然后访问:

http://127.0.0.1:5500

评测建议

  1. 先启动后端,再打开前端页面;
  2. 分别测试四则运算、小数、优先级、括号和一元负号;
  3. 输入非法表达式和除零表达式,检查页面错误信息;
  4. 打开 History,确认成功计算已经写入数据库;
  5. 刷新前端后重新打开 History,确认记录仍然存在;
  6. 删除一条记录,确认页面重新查询并显示最新数据;
  7. 停止后端,再点击等号,确认前端不能独立产生新的有效结果。

成品展示

以下图片来自本地运行的真实前后端程序,使用独立的测试数据库生成。

1 主界面

主界面包含表达式输入框、结果区域、四列按键区和历史记录入口。

2 加法

前端发送12+8,后端返回结果 20。

3 减法

表达式20-7的后端计算结果为 13。

4 乘法

页面使用乘号×展示表达式,API 内部使用*。

5 除法

页面使用除号÷,后端按照二元除法进行求值。

6 小数

表达式12.5/2返回 6.25。

7 运算符优先级

1+2*3返回 7,说明乘法优先于加法。

8 括号

(1+2)*3返回 9,括号改变了默认计算顺序。

9 一元负号

3*-2返回 -6,解析器能够区分一元负号和二元减法。

10 非法表达式

1+*2被语法状态机拒绝,错误直接显示在结果区。

11 除零错误

10/(3-3)在求值阶段触发除零检查,不会写入历史记录。

12 历史记录

History 面板从右侧打开,按最新记录优先的顺序展示表达式、结果和时间。

13 刷新后历史仍然存在

刷新前端页面后重新查询数据库,之前的记录仍然存在,说明历史不是只保存在浏览器内存中。

14 删除指定历史记录

删除一条记录后,前端再次请求历史接口并刷新列表。最上方的3*-2记录已经被移除。

需求分析

功能需求

本系统需要实现四类主要功能:

  1. 后端完成加、减、乘、除;
  2. 后端解析包含小数、括号、优先级和一元正负号的复合表达式;
  3. 每次成功计算后把表达式、结果和时间写入数据库;
  4. 前端查询历史记录,并通过记录 ID 删除指定数据。

前后端分离要求

前端只发送原始表达式,不发送由浏览器计算得到的结果。停止后端后,页面仍然可以输入字符和点击按钮,但不能得到新的有效计算结果。

安全要求

后端不能使用eval、exec或类似机制执行用户输入。本项目实现了自己的 Tokenizer、Parser 和 Calculator,用户字符串不会作为通用程序代码执行。

非功能需求

  • 界面在桌面端和移动端保持可用;
  • API 使用清晰的 HTTP 方法和 JSON 数据;
  • 历史记录必须持久化;
  • 模块职责应清晰,方便测试和维护;
  • README 应使助教能够独立构建和运行项目。

总体架构

用户 │ ▼ Web 前端 HTML CSS JavaScript │ │ HTTP JSON ▼ C++ HTTP 服务 │ ├── 请求校验与路由 ├── Tokenizer ├── ExpressionParser ├── Calculator └── Database │ ▼ SQLite

前端和后端是两个独立程序。前端通过 Fetch API 访问三个后端接口;后端完成解析、求值和数据访问后返回 JSON。

功能结构图

计算器系统 │ ├── 表达式输入与编辑 │ ├── 数字和小数 │ ├── 四则运算符 │ ├── 括号 │ ├── 清空 │ └── 删除末尾字符 │ ├── 表达式计算 │ ├── JSON 与字段校验 │ ├── 词法分析 │ ├── 语法检查 │ ├── 中缀转后缀 │ └── 栈求值 │ ├── 结果与错误展示 │ ├── 成功结果 │ ├── 输入错误 │ └── 后端或数据库错误 │ └── 历史记录管理 ├── 保存成功计算 ├── 查询全部记录 ├── 刷新后恢复记录 └── 删除指定记录

项目结构

前端仓库

832401105_calculator_frontend/ ├── index.html ├── css/ │ └── style.css ├── js/ │ ├── api.js │ ├── ui.js │ └── app.js ├── docs/ │ └── 前端设计文档_v1.md ├── README.md └── codestyle.md

api.js只处理网络请求,ui.js保存表达式并更新 DOM,app.js绑定事件并协调 API 与 UI。

后端仓库

832401105_calculator_backend/ ├── CMakeLists.txt ├── src/ │ ├── main.cpp │ ├── parser/ │ │ ├── Token.h │ │ ├── Tokenizer.h │ │ ├── Tokenizer.cpp │ │ ├── ExpressionParser.h │ │ └── ExpressionParser.cpp │ ├── calculator/ │ │ ├── Calculator.h │ │ └── Calculator.cpp │ ├── database/ │ │ ├── Database.h │ │ └── Database.cpp │ ├── api/ │ │ └── ApiServer.h/.cpp │ ├── common/ │ │ └── Error.h/Result.h │ └── test/ │ ├── tokenizer_test.cpp │ ├── parser_test.cpp │ ├── calculator_test.cpp │ └── database_test.cpp ├── third_party/ │ ├── httplib.h │ ├── json.hpp │ └── sqlite/ ├── README.md └── codestyle.md

当前 HTTP 路由仍直接写在main.cpp中,ApiServer、Error和Result是预留文件。它们体现了后续分层方向,但不能描述为已经完成的独立模块。

前端设计

前端使用 HTML5、CSS3 和原生 JavaScript,不依赖 npm 或前端框架。

页面结构

  • 表达式输入框允许直接键盘编辑;
  • 结果区显示计算结果或错误;
  • 四列网格提供数字、运算符、括号、清空、退格和等号;
  • History 按钮打开右侧历史面板;
  • 遮罩使历史面板打开时的主界面不可操作。

表达式状态

ui.js中的rawExpression保存后端格式。显示时把*和/转换为×和÷,文本框输入后再转换回 API 格式。

当前按钮输入追加到表达式末尾,退格按钮删除最后一个字符。直接编辑文本框时可以使用浏览器原生光标,但按键输入尚未按照selectionStart插入到当前光标位置。

请求状态

计算期间等号按钮会被禁用并显示...。请求结束后由finally恢复按钮,避免重复提交和按钮无法恢复的问题。

后端设计

后端使用 C++17。主要依赖为:

组件版本用途
cpp-httplib0.58.0HTTP Server 与路由
nlohmann/json3.12.0JSON 校验、解析和序列化
SQLite3.53.4历史记录持久化

后端启动时打开data/calculator.db、初始化数据表、注册路由并监听127.0.0.1:8080。只有表达式成功计算且历史记录成功写入后,计算接口才返回成功。

API 设计

方法路径作用成功响应
POST/api/calculate校验并计算表达式{ "success": true, "result": 9.0 }
GET/api/history查询全部历史记录JSON 数组
DELETE/api/history/{id}删除指定记录{ "success": true, "deleted_id": 1 }
OPTIONS.*CORS 预检204

计算请求:

{"expression":"(1+2)*3"}

计算错误:

{"success":false,"error":"Division by zero"}

历史查询接口当前直接返回数组:

[{"id":1,"expression":"(1+2)*3","result":9.0,"created_at":"2026-10-07 15:05:00"}]

输入和计算错误使用 400,数据库保存或查询异常使用 500。当前删除接口没有检查受影响行数,因此不存在的 ID 也会返回成功;这是后续需要修复的接口语义问题。

数据库设计

系统只使用一张历史表:

CREATETABLEIFNOTEXISTScalculation_history(idINTEGERPRIMARYKEYAUTOINCREMENT,expressionTEXTNOTNULL,resultREALNOTNULL,created_atTEXTNOTNULLDEFAULTCURRENT_TIMESTAMP);
字段说明
id自增主键,也是删除接口使用的记录标识
expression用户提交的原始表达式
result后端计算得到的double结果
created_atSQLite 自动生成的 UTC 时间

查询使用ORDER BY id DESC,最新记录排在最前面。插入和删除使用预处理语句和参数绑定,不通过字符串拼接 SQL。

表达式计算模块

完整流程如下:

原始字符串 ↓ Tokenizer ↓ Token 序列 ↓ 语法状态机和一元运算符识别 ↓ 调度场算法 ↓ 后缀表达式 ↓ 数字栈求值

Tokenizer

Tokenizer 从左到右识别数字、+ - * /和括号。数字中出现第二个小数点时抛出Invalid number format,其他不支持的字符抛出Invalid character。

当前实现只忽略普通空格,且小数必须以数字开头。因此0.5合法,.5不合法,5.可以解析。

语法状态机

Parser 使用两个状态:

  • ExpectOperand:允许数字、左括号、一元正号和一元负号;
  • ExpectOperator:允许二元运算符和右括号。

同一个-在ExpectOperand状态下转换为u-,在ExpectOperator状态下保留为二元减法。这个设计同时完成语法校验和一元运算符识别。

中缀转后缀

调度场算法使用输出数组和运算符栈。优先级为:

运算符优先级
u+、u-3
*、/2
+、-1

例如:

3 * -2 + (4 - 1)

转换为:

3 2 u- * 4 1 - +

后缀求值

Calculator 使用std::stack<double>。数字压栈;一元运算符弹出一个值;二元运算符先弹出右操作数,再弹出左操作数。所有 Token 处理结束后,栈中必须只剩一个结果。

整体时间复杂度和额外空间复杂度都是O(n)。

计算历史模块

成功计算后的处理顺序为:

计算成功 ↓ Database::insertHistory ↓ SQLite 写入表达式和结果 ↓ API 返回成功结果

查询历史时,后端读取全部记录并生成 JSON 数组。前端使用事件委托处理动态生成的删除按钮。删除成功后不直接假设本地列表正确,而是再次请求GET /api/history。

刷新页面后,前端的 JavaScript 状态会被清空,但 SQLite 文件仍然存在。重新打开 History 时,记录由数据库重新读取,因此满足持久化要求。

异常处理

错误检测位置HTTP 状态
JSON 非法HTTP 处理层400
缺少expressionHTTP 处理层400
expression不是字符串HTTP 处理层400
空表达式Parser400
非法字符Tokenizer400
非法数字格式Tokenizer400
表达式结构错误Parser400
括号不匹配Parser400
除零Calculator400
历史保存失败Database500
历史查询或删除异常Database500

前端优先显示响应中的error字段。计算错误显示在结果区,历史请求错误显示在历史面板中,不使用弹窗打断输入。

前后端交互流程

计算

用户输入表达式并点击等号 ↓ 前端 POST /api/calculate ↓ 后端校验 JSON 和字段 ↓ Tokenizer → Parser → Calculator ↓ 成功后写入 SQLite ↓ 返回 JSON 结果 ↓ 前端更新结果区

查询历史

用户点击 History ↓ 前端打开面板并显示 Loading ↓ GET /api/history ↓ 后端按 ID 倒序查询 SQLite ↓ 前端渲染记录

删除记录

用户点击某条记录的 Delete ↓ DELETE /api/history/{id} ↓ 后端删除数据库记录 ↓ 前端再次 GET /api/history ↓ 显示最新列表

测试

已执行的核心链路测试

测试输入或操作预期结果实际结果
12+820通过
20-713通过
6*742通过
84/127通过
12.5/26.25通过
1+2*37通过
(1+2)*39通过
3*-2-6通过
1+*2Syntax error通过
10/(3-3)Division by zero和 HTTP 400通过
刷新后查询历史记录仍存在通过
删除指定记录数据库删除并刷新列表通过

这些测试使用独立的临时数据库,不会修改开发数据库。

当前测试结构

后端src/test中有 Tokenizer、Parser、Calculator 和 Database 四个开发测试程序。当前 CMake 默认只构建database_test,还没有接入 CTest 或其他自动化测试框架。后续应把交互式测试改为固定输入和断言,形成可重复的回归测试。

关键代码说明

1 前端只发送表达式

asyncfunctioncalculateExpression(expression){constresponse=awaitfetch(`${API_BASE_URL}/api/calculate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({expression:expression})});constdata=awaitresponse.json();if(!response.ok){thrownewError(data.error||"Calculation failed");}returndata;}

前端没有计算表达式,只提交原始字符串并显示后端返回的result。这样满足前后端分离要求,也避免浏览器和服务端维护两套计算规则。

2 后端计算流水线

autotokens=Tokenizer::tokenize(expression);autopostfix=ExpressionParser::toPostfix(tokens);result=Calculator::evaluate(postfix);

分词、解析和求值被拆成三个步骤。每一层只处理一种问题,便于定位非法字符、语法错误或计算错误。

3 一元运算符识别

caseTokenKind::Plus:token.text="u+";token.type=TokenKind::UnaryPlus;operators.push(token);break;caseTokenKind::Minus:token.text="u-";token.type=TokenKind::UnaryMinus;operators.push(token);break;

这段逻辑只会在 Parser 期待操作数时执行。因此表达式开头、左括号后和二元运算符后的+/-会被识别为一元运算符。

4 二元运算的操作数顺序

doubleright=numbers.top();numbers.pop();doubleleft=numbers.top();numbers.pop();

后缀表达式求值时,先弹出的是右操作数。减法和除法不能交换左右顺序,因此代码显式使用left operator right。

5 除零检查

caseTokenKind::Divide:if(right==0){throwstd::runtime_error("Division by zero");}result=left/right;break;

除零属于求值阶段错误。异常被 HTTP 处理层转换为 400 响应,失败计算不会写入数据库。

6 SQLite 参数绑定

sqlite3_bind_text(statement,1,expression.c_str(),-1,SQLITE_TRANSIENT);sqlite3_bind_double(statement,2,result);

SQL 模板与数据分开,表达式不会被直接拼接进 SQL 字符串。这样既使代码更清晰,也减少 SQL 注入风险。

7 删除后重新查询

awaitdeleteHistory(id);awaitloadHistory();

数据库是历史记录的最终数据源。删除成功后重新查询,可以保证页面展示与数据库一致。

开发过程与收获

从 Git 提交顺序可以看到,我先建立前后端仓库和基础目录,然后依次完成 HTTP 与 JSON 环境、表达式解析、后缀求值、一元运算符、前后端计算联调、历史记录和界面完善。这种顺序先打通核心计算,再增加持久化和展示,减少了同时调试多个模块的复杂度。

一元负号与减法的歧义

-既可以表示减法,也可以表示负号。仅根据字符本身无法判断。我使用ExpectOperand和ExpectOperator状态记录当前位置需要什么,从而把-5和3*-2中的减号转成u-。这让我更清楚地理解了词法分析和语法分析的区别。

运算符优先级和括号

直接边读取边计算很容易在优先级和括号上出错。调度场算法把中缀表达式转换成后缀表达式后,Calculator 只需要处理顺序 Token。解析和计算职责分开后,错误更容易定位。

前端显示符号与 API 符号

界面使用×和÷更符合计算器习惯,但后端只识别*和/。我在 UI 层增加formatExpression()和normalizeExpression(),保持 API 表达式简单,同时不影响展示效果。

前后端联调

前端和后端运行在不同端口,浏览器请求需要 CORS。后端统一设置允许来源、方法和Content-Type,并处理 OPTIONS 预检。联调过程也说明接口字段必须保持一致,例如当前错误字段是error,历史查询成功响应是数组而不是带history字段的对象。

数据库路径和持久化

SQLite 文件使用相对路径data/calculator.db。这要求程序从正确的工作目录启动,并提前创建data目录。刷新页面后重新读取数据库的测试让我确认了“页面状态”和“持久化状态”是两个不同层次。

主要收获

  • 理解了前后端分离不是目录分开,而是职责和计算位置分开;
  • 掌握了 HTTP 方法、状态码、JSON 和 CORS 的基本使用;
  • 实现了 Tokenizer、语法状态机、调度场算法和后缀表达式求值;
  • 学会使用 SQLite 预处理语句进行持久化操作;
  • 认识到 README、API 契约、测试和实际代码需要同步维护。

不足与后续改进

  1. 完善部署配置管理。项目已经部署到公网,但开发环境中的后端监听地址和前端 API 地址仍采用固定配置。后续可统一改为环境配置,并增加 HTTPS 反向代理。
  2. 修复历史记录渲染安全问题。当前部分历史内容通过innerHTML插入,应改用createElement()和textContent。
  3. 完善删除接口语义。使用sqlite3_changes()判断是否实际删除记录,不存在时返回 404。
  4. 拆分 API 层。将main.cpp中的路由和处理函数迁移到ApiServer,减少入口文件职责。
  5. 统一错误类型。实现Error.h和Result.h,用错误码稳定映射 HTTP 状态,而不是只传递字符串异常。
  6. 增加自动化测试。把四个开发测试接入 CTest,并增加 API 与前端端到端测试。
  7. 完善输入体验。让按键输入和退格遵循文本光标位置,增加 Enter 提交和更明显的键盘焦点样式。
  8. 改善历史长列表。增加滚动容器、分页或数量限制,并完善 Delete 按钮样式。
  9. 限制资源使用。对请求体和表达式长度设置上限,公网部署时限制 CORS 来源和请求频率。
  10. 统一时间和结果格式。将 SQLite UTC 时间转换为本地时间,并规定浮点结果的有效数字和尾零规则。

总结

本项目完成了一个前后端分离的 Web 计算器。前端负责输入、展示和历史交互;C++ 后端独立完成表达式校验、调度场转换和栈求值;SQLite 保存成功计算并支持查询与删除。四则运算、复合表达式、小数、括号、一元负号、非法表达式、除零、历史持久化和指定记录删除均已通过本地核心链路测试。

项目已经完成公网部署和作业提交,助教可以通过公开地址进行评测。后续仍可继续完善自动化测试、接口语义、安全渲染和部署配置,使项目从课程作业进一步发展为更稳定的 Web 应用。

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

双十一蓝牙耳机推荐:5 款在售 TWS 按场景选购(含参数对照)

双十一选蓝牙耳机&#xff0c;先定场景再定型号&#xff1a;通勤看 ANC 降噪&#xff0c;办公看佩戴时长&#xff0c;常打电话看 ENC 通话降噪&#xff0c;运动看防水和佩戴稳固。预算百元到两百元、安卓用户想兼顾听歌和户外通话&#xff0c;可以把梵洛音 CZA06作为入门备选&a…

作者头像 李华