173
社区成员
发帖
与我相关
我的任务
分享|
Item |
Content |
|---|---|
|
Course |
Software Engineering |
|
Assignment Name |
Development of a Frontend-Backend Separation Calculator System |
|
Assignment Goal |
Master frontend-backend separation architecture, HTTP API communication, database persistence, exception handling, and project deployment workflow |
|
Tech Stack |
Frontend: HTML + CSS + Vanilla JavaScript Backend: Python Flask + Flask-SQLAlchemy Database: SQLite |
GitHub Repository Links
Code Style Guidelines
PSP Personal Software Process Table
Project Overview and Requirements Analysis
System Overall Design
Core Feature Implementation
Key Code Analysis
Feature Testing and Demo
Deployment Instructions
Development Issues, Solutions, and Reflections
Replace with your own repository links before submission.
Frontend Repository: https://github.com/xuntachuan/calculator-frontend
Backend Repository: https://github.com/xuntachuan/calculator-backend
Frontend Code Style: https://github.com/xuntachuan/calculator-frontend/blob/main/codestyle.md
Backend Code Style: https://github.com/xuntachuan/calculator-backend/blob/main/codestyle.md
The backend follows the Python PEP 8 coding standard, and the frontend follows the Airbnb JavaScript Style Guide.
Backend code uses snake_case naming with semantically clear function and variable names. Frontend JavaScript uses camelCase naming, HTML is structured in layers, and CSS styles are uniformly placed in the page head. Each repository contains a codestyle.md document that uniformly constrains indentation, comments, and naming to ensure code readability and facilitate future maintenance.
|
Phase |
Estimated (min) |
Actual (min) |
Description |
|---|---|---|---|
|
Requirements Analysis |
20 |
15 |
Clarified the hard requirements of frontend-backend separation: the frontend only handles UI interaction, all calculation logic is implemented on the backend; history persistence and exception handling are required |
|
System Architecture Design |
20 |
25 |
Designed REST-style API endpoints, SQLite table schema, Reverse Polish Notation calculation module, and CORS handling strategy |
|
Backend Development |
90 |
110 |
Implemented tokenization, infix-to-postfix conversion, postfix expression evaluation; wrote database models, three API endpoints, and exception handling |
|
Frontend Development |
60 |
55 |
Calculator UI layout, button click expression concatenation, fetch request encapsulation, history rendering and deletion interaction |
|
Integration Testing |
40 |
45 |
Tested arithmetic operations, nested parentheses, decimals, unary minus; tested division by zero, illegal characters, mismatched parentheses; tested history add/delete |
|
Deployment |
20 |
15 |
Installed dependencies, started Flask service, verified frontend-backend CORS communication, database auto-created tables |
|
Blog Writing and Organization |
60 |
70 |
Organized architecture, code, test screenshots, and development summary |
|
Total |
310 |
335 |
—— |
This project implements a frontend-backend separation online calculator based on Flask + vanilla frontend. The frontend performs no calculations — it only provides a visual interface, receives user input, and sends JSON requests to the backend. The backend receives expressions and uses the Reverse Polish Notation (postfix expression) algorithm to parse and evaluate expressions, without using eval, to avoid code injection risks. After a successful calculation, the expression, result, and timestamp are stored in a SQLite database for persistent history, supporting viewing and deleting history records.
Strict frontend-backend separation with HTTP + JSON API communication
All mathematical calculation logic resides on the backend; the frontend only handles display and request sending
Supports arithmetic operations, decimals, multi-level nested parentheses, and unary minus (-5, 3*-2)
No dangerous eval function — manually implemented lexical analysis + Reverse Polish Notation calculation for security
Calculation records are persistently stored; records survive page refresh and service restart
Supports viewing all history records and deleting individual history records
Complete exception handling: division by zero, illegal characters, mismatched parentheses, malformed expressions, network errors
Backend enables CORS so the frontend webpage can directly access backend APIs
Frontend Modules
UI Module: calculator buttons, input field, result display area, history panel
Interaction Module: button expression concatenation, clear function
Network Request Module: send calculation requests, fetch history list, delete history records
Rendering Module: render history data returned by the backend to the page, auto-refresh list after deletion
Backend Modules
Lexical Tokenization Module tokenize: splits expressions into tokens, identifies numbers, operators, unary minus, and filters illegal characters
Infix-to-Postfix Module infix_to_postfix: uses a stack to convert infix math expressions to Reverse Polish Notation, handling operator precedence and parentheses
Postfix Evaluation Module calc_postfix: evaluates postfix expressions with a stack, catching division by zero and format errors
Database Model Module: defines the history table using Flask-SQLAlchemy
API Module: calculation endpoint, history query endpoint, history deletion endpoint
Exception Handling Module: uniformly catches various calculation exceptions and returns standardized JSON error codes
Table: CalcRecord
|
Field |
Type |
Description |
|---|---|---|
|
id |
Integer (primary key, auto-increment) |
Unique record ID |
|
expression |
String(256) |
Original expression entered by the user |
|
result |
String(256) |
Calculation result |
|
create_time |
DateTime |
Record creation time, defaults to current UTC time |
The backend manually implements the Reverse Polish Notation algorithm, following mathematical operator precedence (multiplication and division before addition and subtraction). Supports multi-level nested parentheses, decimal operations, and unary minus.
Examples: (1+2.5)*(-3+7), 10/2 both calculate correctly.
Does not call Python's built-in eval to execute user strings. Manually tokenizes and parses syntax, intercepts illegal characters, and prevents code injection security risks.
On every successful calculation, the backend automatically stores the expression, result, and timestamp in the SQLite database. The frontend automatically requests the backend API to read history on page load; data survives page refresh because it is stored in the backend database, not dependent on frontend localStorage caching.
The frontend clicks the delete button and sends a DELETE request with the record ID to the backend. The backend queries the corresponding record; if it exists, it deletes the database record and commits the transaction. After successful deletion, the frontend auto-refreshes the history list. If the record does not exist, a 404 error is returned.
Division by zero: catches ZeroDivisionError when the divisor is near zero and returns a prompt
Illegal characters: identifies non-digit/non-operator characters during tokenization and raises an exception
Mismatched parentheses: detects inconsistent parenthesis counts during infix-to-postfix conversion
Malformed expressions: e.g., consecutive operators, missing operands
Network errors: the frontend catches failed fetch requests and prompts to check the backend service
# Operator precedence
priority = {'+': 1, '-': 1, '*': 2, '/': 2}
def tokenize(expr):
"""Tokenize: split numbers and operators, handle unary minus"""
tokens = []
i = 0
n = len(expr)
while i < n:
c = expr[i]
if c.isspace():
i += 1
continue
if c in '()+ -*/':
# Unary minus detection
if c == '-' and (i == 0 or expr[i-1] in '(*+-/'):
tokens.append('u-')
else:
tokens.append(c)
i += 1
elif c.isdigit() or c == '.':
# Read number (supports decimals)
j = i
while j < n and (expr[j].isdigit() or expr[j] == '.'):
j += 1
tokens.append(expr[i:j])
i = j
else:
raise ValueError("Contains illegal characters")
return tokens
def infix_to_postfix(tokens):
"""Convert infix to postfix (Reverse Polish Notation)"""
postfix = []
op_stack = []
for t in tokens:
if t.replace('.', '').isdigit():
postfix.append(t)
elif t == '(':
op_stack.append(t)
elif t == ')':
while op_stack and op_stack[-1] != '(':
postfix.append(op_stack.pop())
if not op_stack:
raise ValueError("Mismatched parentheses")
op_stack.pop()
else: # operators + - * / u-
while op_stack and op_stack[-1] != '(' and priority.get(op_stack[-1], 3) >= priority.get(t, 3):
postfix.append(op_stack.pop())
op_stack.append(t)
while op_stack:
top = op_stack.pop()
if top == '(':
raise ValueError("Mismatched parentheses")
postfix.append(top)
return postfix
def calc_postfix(postfix):
"""Evaluate postfix expression"""
stk = []
for token in postfix:
if token == 'u-':
if not stk:
raise ValueError("Expression error")
val = float(stk.pop())
stk.append(-val)
elif token in '+-*/':
if len(stk) < 2:
raise ValueError("Expression error")
b = float(stk.pop())
a = float(stk.pop())
if token == '+':
res = a + b
elif token == '-':
res = a - b
elif token == '*':
res = a * b
elif token == '/':
if abs(b) < 1e-10:
raise ZeroDivisionError("Division by zero")
res = a / b
stk.append(res)
else:
stk.append(float(token))
if len(stk) != 1:
raise ValueError("Malformed expression")
return stk[0]
def calculate_expression(expr):
tokens = tokenize(expr)
post = infix_to_postfix(tokens)
res = calc_postfix(post)
return res
@app.post("/api/calc")
def api_calc():
data = request.get_json()
if not data or "expression" not in data:
return jsonify({"code": 400, "msg": "Missing expression parameter"}), 400
expr = data["expression"].strip()
try:
result = calculate_expression(expr)
res_str = f"{result:.6f}".rstrip('0').rstrip('.') if '.' in f"{result:.6f}" else f"{result:.6f}"
# Save to database
rec = CalcRecord(expression=expr, result=res_str)
db.session.add(rec)
db.session.commit()
return jsonify({"code": 200, "data": {"result": res_str}})
except ZeroDivisionError:
return jsonify({"code": 400, "msg": "Division by zero"}), 400
except ValueError as e:
return jsonify({"code": 400, "msg": f"Illegal expression: {str(e)}"}), 400
except Exception as e:
return jsonify({"code": 400, "msg": f"Calculation error: {str(e)}"}), 400
# Query history endpoint
@app.get("/api/history")
def api_get_history():
records = CalcRecord.query.order_by(CalcRecord.create_time.desc()).all()
return jsonify({"code": 200, "data": [r.to_dict() for r in records]})
# Delete history endpoint
@app.delete("/api/history/<int:rid>")
def api_del_history(rid):
rec = CalcRecord.query.get(rid)
if not rec:
return jsonify({"code": 404, "msg": "Record not found"}), 404
db.session.delete(rec)
db.session.commit()
return jsonify({"code": 200, "msg": "Deleted successfully"})
// Backend URL, modify according to actual situation
const BACKEND_URL = "http://127.0.0.1:5000";
const exprInput = document.getElementById("input-exp");
const resultDiv = document.getElementById("result");
const historyWrap = document.getElementById("history-list");
window.onload = () => {
loadHistory();
}
function appendChar(c) {
exprInput.value += c;
}
function clearAll() {
exprInput.value = "";
resultDiv.innerText = "Result";
}
// Click equals, request backend to calculate
async function calcEqual() {
const expr = exprInput.value.trim();
if (!expr) {
resultDiv.innerText = "Please enter an expression";
return;
}
try {
const resp = await fetch(`${BACKEND_URL}/api/calc`, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({expression: expr})
})
const json = await resp.json();
if (json.code === 200) {
resultDiv.innerText = json.data.result;
loadHistory(); // Refresh history after successful calculation
} else {
resultDiv.innerText = json.msg;
}
} catch (err) {
resultDiv.innerText = "Network request failed, please check the backend";
console.error(err);
}
}
// Fetch history list
async function loadHistory() {
try {
const resp = await fetch(`${BACKEND_URL}/api/history`);
const json = await resp.json();
if (json.code === 200) {
renderHistory(json.data);
}
} catch (e) {
historyWrap.innerHTML = "<p>Failed to load history</p>";
}
}
// Delete a single record
async function delRecord(id) {
if (!confirm("Are you sure you want to delete this record?")) return;
try {
await fetch(`${BACKEND_URL}/api/history/${id}`, {method: "DELETE"});
loadHistory();
} catch (e) {
alert("Delete failed");
}
}
function renderHistory(list) {
if (list.length === 0) {
historyWrap.innerHTML = "<p>No records yet</p>";
return;
}
let html = "";
list.forEach(item => {
html += `
<div class="history-item">
<div>${item.expression} = ${item.result}<br><small>${item.create_time}</small></div>
<button class="del-btn" onclick="delRecord(${item.id})">Delete</button>
</div>
`
})
historyWrap.innerHTML = html;
}








Calculator main interface: left-right layout, calculator button panel on the left, history panel on the right; supports clicking buttons to input expressions and manual input.
Basic arithmetic test: 1+2, 10-3.5, 4*5, 9/2 all produce correct results.
Compound expression and parentheses test: (1+2)*3, 1+2*3 — precedence follows mathematical rules; supports multi-level parentheses.
Unary minus test: -5+9, 4*-2 — minus sign is correctly recognized.
Division by zero test: entering 10/0, the backend catches ZeroDivisionError and the frontend displays a division by zero error prompt.
Illegal expression test: mismatched parentheses, illegal characters — the backend returns corresponding error messages.
History persistence test: calculations are automatically saved to the database; after page refresh, history records remain.
Delete record test: clicking the delete button, after confirmation the database record is deleted and the page history list auto-updates; deleting a non-existent ID returns 404.
Exception scenario test: when the backend is not running, the frontend prompts network request failure.
Install backend dependencies
pip install flask flask-sqlalchemy flask-cors
Run the backend program: python main.py. The backend starts at http://127.0.0.1:5000 and automatically creates the calc_history.db database file.
Open the frontend HTML file directly in a browser. Ensure BACKEND_URL in the frontend JavaScript matches the backend address.
The backend enables CORS, so local webpages can directly access the API without additional proxy configuration.
Difficulty in recognizing unary minus
Initially, the tokenization module could not distinguish between the subtraction operator and the negative sign, e.g., 3*-2 was parsed incorrectly. Solution: added detection logic — when - appears at the beginning of the expression or after (, *, +, it is judged as a unary minus, marked as u-, and handled separately during postfix evaluation.
Frontend-backend CORS request error
When the frontend opened the HTML and accessed the backend API, the browser reported a CORS restriction. Solution: the backend introduced flask-cors and executed CORS(app) to enable global CORS, allowing the frontend webpage to access the API.
Handling mismatched parentheses in Reverse Polish Notation
When a user entered (1+2 (missing closing parenthesis), the program crashed directly. Solution: during the infix-to-postfix stack operation, detect leftover opening parentheses and actively raise a ValueError with "Mismatched parentheses", which is uniformly caught and returned to the frontend.
Database time formatting
The database stores UTC time. The to_dict function formats the time, converting datetime to a readable string that the frontend directly displays in YYYY-MM-DD HH:MM:SS format.
Trailing zeros in floating-point results
Calculation results like 4.0 were desired to be simplified to 4. Solution: format the result string to automatically strip trailing zeros and decimal points for better display.
Compared to directly using eval, manually implementing lexical analysis and stack-based expression evaluation helped me understand the underlying principles of expression parsing while avoiding the security risk of code injection. Through REST-style APIs and JSON data interaction, I became familiar with the engineering practices of interface design, CORS, database ORM operations, and exception handling.
During the integration process, I also learned frontend-backend joint debugging and troubleshooting, distinguishing between frontend page errors, network errors, and backend business exceptions. The entire project followed the software engineering process — from requirements analysis, architecture design, coding, testing to documentation writing — with standardized development that enhanced my engineering development mindset.