ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

第一次作业_前后端分离计算器系统_中文版 第一次作业 前后端分离计算器系统学号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 minWeb 前端基础30 min78 min前端界面开发60 min146 minHTTP API JSON 学习60 min133 minC 后端搭建120 min217 min表达式解析模块75 min287 minSQLite 学习与设计180 min96 min计算历史模块75 min103 min前后端联调120 min139 min异常处理与完善60 min74 min测试60 min88 min部署30 min204 minREADME 和 codestyle45 min20 min博客撰写60 min30 min合计1015 min1734 min部署和评测说明公网地址公网访问地址http://43.128.135.64项目已经完成公网部署助教可以通过上述地址访问并测试系统。以下本地运行说明仍保留用于代码检查、独立构建和问题排查。本地运行环境支持 C17 的编译器CMake 3.16 或更高版本任意静态文件服务器现代浏览器无需安装额外的 HTTP、JSON 或 SQLite 包后端仓库已经包含相关源码。后端启动在后端仓库根目录执行cmake-Emake_directory data cmake-S.-Bbuild cmake--buildbuildWindows 启动命令.\build\calculator_backend.exeLinux 或 macOS 启动命令./build/calculator_backend后端监听http://127.0.0.1:8080。程序必须从后端仓库根目录启动使相对路径data/calculator.db能够正确解析。前端启动在前端仓库根目录启动静态文件服务器python-mhttp.server5500然后访问http://127.0.0.1:5500评测建议先启动后端再打开前端页面分别测试四则运算、小数、优先级、括号和一元负号输入非法表达式和除零表达式检查页面错误信息打开 History确认成功计算已经写入数据库刷新前端后重新打开 History确认记录仍然存在删除一条记录确认页面重新查询并显示最新数据停止后端再点击等号确认前端不能独立产生新的有效结果。成品展示以下图片来自本地运行的真实前后端程序使用独立的测试数据库生成。1 主界面主界面包含表达式输入框、结果区域、四列按键区和历史记录入口。2 加法前端发送128后端返回结果 20。3 减法表达式20-7的后端计算结果为 13。4 乘法页面使用乘号×展示表达式API 内部使用*。5 除法页面使用除号÷后端按照二元除法进行求值。6 小数表达式12.5/2返回 6.25。7 运算符优先级12*3返回 7说明乘法优先于加法。8 括号(12)*3返回 9括号改变了默认计算顺序。9 一元负号3*-2返回 -6解析器能够区分一元负号和二元减法。10 非法表达式1*2被语法状态机拒绝错误直接显示在结果区。11 除零错误10/(3-3)在求值阶段触发除零检查不会写入历史记录。12 历史记录History 面板从右侧打开按最新记录优先的顺序展示表达式、结果和时间。13 刷新后历史仍然存在刷新前端页面后重新查询数据库之前的记录仍然存在说明历史不是只保存在浏览器内存中。14 删除指定历史记录删除一条记录后前端再次请求历史接口并刷新列表。最上方的3*-2记录已经被移除。需求分析功能需求本系统需要实现四类主要功能后端完成加、减、乘、除后端解析包含小数、括号、优先级和一元正负号的复合表达式每次成功计算后把表达式、结果和时间写入数据库前端查询历史记录并通过记录 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.mdapi.js只处理网络请求ui.js保存表达式并更新 DOMapp.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恢复按钮避免重复提交和按钮无法恢复的问题。后端设计后端使用 C17。主要依赖为组件版本用途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:(12)*3}计算错误{success:false,error:Division by zero}历史查询接口当前直接返回数组[{id:1,expression:(12)*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 序列 ↓ 语法状态机和一元运算符识别 ↓ 调度场算法 ↓ 后缀表达式 ↓ 数字栈求值TokenizerTokenizer 从左到右识别数字、 - * /和括号。数字中出现第二个小数点时抛出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::stackdouble。数字压栈一元运算符弹出一个值二元运算符先弹出右操作数再弹出左操作数。所有 Token 处理结束后栈中必须只剩一个结果。整体时间复杂度和额外空间复杂度都是O(n)。计算历史模块成功计算后的处理顺序为计算成功 ↓ Database::insertHistory ↓ SQLite 写入表达式和结果 ↓ API 返回成功结果查询历史时后端读取全部记录并生成 JSON 数组。前端使用事件委托处理动态生成的删除按钮。删除成功后不直接假设本地列表正确而是再次请求GET /api/history。刷新页面后前端的 JavaScript 状态会被清空但 SQLite 文件仍然存在。重新打开 History 时记录由数据库重新读取因此满足持久化要求。异常处理错误检测位置HTTP 状态JSON 非法HTTP 处理层400缺少expressionHTTP 处理层400expression不是字符串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 ↓ 显示最新列表测试已执行的核心链路测试测试输入或操作预期结果实际结果12820通过20-713通过6*742通过84/127通过12.5/26.25通过12*37通过(12)*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){constresponseawaitfetch(${API_BASE_URL}/api/calculate,{method:POST,headers:{Content-Type:application/json},body:JSON.stringify({expression:expression})});constdataawaitresponse.json();if(!response.ok){thrownewError(data.error||Calculation failed);}returndata;}前端没有计算表达式只提交原始字符串并显示后端返回的result。这样满足前后端分离要求也避免浏览器和服务端维护两套计算规则。2 后端计算流水线autotokensTokenizer::tokenize(expression);autopostfixExpressionParser::toPostfix(tokens);resultCalculator::evaluate(postfix);分词、解析和求值被拆成三个步骤。每一层只处理一种问题便于定位非法字符、语法错误或计算错误。3 一元运算符识别caseTokenKind::Plus:token.textu;token.typeTokenKind::UnaryPlus;operators.push(token);break;caseTokenKind::Minus:token.textu-;token.typeTokenKind::UnaryMinus;operators.push(token);break;这段逻辑只会在 Parser 期待操作数时执行。因此表达式开头、左括号后和二元运算符后的/-会被识别为一元运算符。4 二元运算的操作数顺序doublerightnumbers.top();numbers.pop();doubleleftnumbers.top();numbers.pop();后缀表达式求值时先弹出的是右操作数。减法和除法不能交换左右顺序因此代码显式使用left operator right。5 除零检查caseTokenKind::Divide:if(right0){throwstd::runtime_error(Division by zero);}resultleft/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 契约、测试和实际代码需要同步维护。不足与后续改进完善部署配置管理。项目已经部署到公网但开发环境中的后端监听地址和前端 API 地址仍采用固定配置。后续可统一改为环境配置并增加 HTTPS 反向代理。修复历史记录渲染安全问题。当前部分历史内容通过innerHTML插入应改用createElement()和textContent。完善删除接口语义。使用sqlite3_changes()判断是否实际删除记录不存在时返回 404。拆分 API 层。将main.cpp中的路由和处理函数迁移到ApiServer减少入口文件职责。统一错误类型。实现Error.h和Result.h用错误码稳定映射 HTTP 状态而不是只传递字符串异常。增加自动化测试。把四个开发测试接入 CTest并增加 API 与前端端到端测试。完善输入体验。让按键输入和退格遵循文本光标位置增加 Enter 提交和更明显的键盘焦点样式。改善历史长列表。增加滚动容器、分页或数量限制并完善 Delete 按钮样式。限制资源使用。对请求体和表达式长度设置上限公网部署时限制 CORS 来源和请求频率。统一时间和结果格式。将 SQLite UTC 时间转换为本地时间并规定浮点结果的有效数字和尾零规则。总结本项目完成了一个前后端分离的 Web 计算器。前端负责输入、展示和历史交互C 后端独立完成表达式校验、调度场转换和栈求值SQLite 保存成功计算并支持查询与删除。四则运算、复合表达式、小数、括号、一元负号、非法表达式、除零、历史持久化和指定记录删除均已通过本地核心链路测试。项目已经完成公网部署和作业提交助教可以通过公开地址进行评测。后续仍可继续完善自动化测试、接口语义、安全渲染和部署配置使项目从课程作业进一步发展为更稳定的 Web 应用。
返回列表