832402212雷彧— First Assignment — Front-End and Back-End Separation Calculator System

832402212雷彧 2026-10-09 16:45:04

Course Information

ItemContent
Course for This AssignmentSoftware Engineering
Assignment Requirementshttps://t.csdn.cn/bJZiK
Objectives of This AssignmentMaster the front-end/back-end separation architecture; implement core calculation logic and database persistence on the back end; communicate via REST APIs; practice deployment and technical blog writing
Other ReferencesFlask official documentation, PEP 8, Airbnb JavaScript Style Guide, MDN Web Docs

目录

  • Course Information
  • 1. Assignment Description
  • 2. Git Repository Links and Code Standards Links
  • 3. PSP Table
  • 4. Presentation of the Finished Product
  • 1. Main Calculator Interface
  • 2. Addition 12+8
  • 3. Subtraction 10-4
  • 4. Multiplication 5*8
  • 5. Division and Decimals 10/4
  • 6. Compound Expression and Precedence 1+2*3
  • 7. Parentheses (1+2)*3
  • 8. Unary Plus/Minus -5+8 and 3*-2
  • 9. Invalid Expression Handling 1++
  • 10. Division by Zero 5÷0
  • 11. Calculation History
  • 12. History Persistence Check
  • 13. Deleting One Record + Clearing All
  • 14. Extended Features
  • 5. Design and Implementation Process
  • 5.1 Requirement Analysis
  • 5.2 Overall Architecture
  • 5.3 Functional Structure Diagram
  • 5.4 API Design
  • 5.5 Database Design
  • 5.6 Calculation Module Design (Core)
  • 5.7 History Module Design
  • 5.8 Exception Handling Design
  • 5.9 Front-End/Back-End Interaction Flow
  • 5.10 Deployment Process
  • 6. Code Explanation
  • 6.1 Back End: Expression Parser (Tokenization + Recursive Descent)
  • 6.2 Back End: Calculation Endpoint (Controller Layer)
  • 6.3 Back End: Database Layer (Parameterized SQL)
  • 6.4 Front End: API Request Wrapper
  • 6.5 Front End: Calculation Flow (Reflecting the Separated Architecture)
  • 6.6 Front End: Re-query After Deleting History
  • 7. Personal Journey and Learnings
  • Appendix: Running Locally


1. Assignment Description

This assignment requires building a calculator system based on a front-end/back-end separation architecture:

  • The front end (a Web page) is responsible only for user interaction: receiving input, sending calculation requests, displaying calculation results, displaying and deleting history records, and showing error messages returned by the back end;
  • The back end (a Flask service) is responsible for all core logic: input validation, mathematical expression parsing and calculation (eval is forbidden), exception handling, and CRUD operations on calculation history in the database;
  • The front end and back end communicate through HTTP REST APIs + JSON;
  • All calculation history is persisted in the back-end SQLite database, so refreshing or restarting the front end never loses it.

One of the core acceptance checks: after the back-end service is stopped, the front end can still accept input, but it cannot independently obtain any new calculation result — this project is implemented exactly according to this principle, and the front end contains no calculation logic at all.


ItemLink
Frontend Repositoryhttps://github.com/huo123657/832402212_calculator_frontend
Backend Repositoryhttps://github.com/huo123657/832402212_calculator_backend
Frontend Code Standardhttps://github.com/huo123657/832402212_calculator_frontend/blob/main/codestyle.md
Backend Code Standardhttps://github.com/huo123657/832402212_calculator_backend/blob/main/codestyle.md
Online AccessFrontend: https://huo123657.github.io/832402212_calculator_frontend/ (Backend: https://huo123657.pythonanywhere.com/)
  • The frontend code standard is based on the Airbnb JavaScript Style Guide and the Google HTML/CSS Style Guide;
  • The backend code standard is based on PEP 8 (the official Python style guide).

3. PSP Table

PSP StageEstimated Time (hours)Actual Time (hours)
Requirement analysis0.50.5
System design (architecture / API / database)11
Backend development: expression parsing module33.5
Backend development: API and database22
Frontend development: UI and styles2.53
Frontend development: interaction logic22.5
Front-end/back-end integration and testing1.52
Deployment11.5
Blog writing22.5
Total15.518.5

The expression parsing module exceeded its estimate: the edge cases of unary plus/minus (e.g. 3*-2, -5+8) and recursive-descent parsing were more numerous than expected, and handling floating-point display errors also took extra time.


4. Presentation of the Finished Product

Screenshot checklist (each image is followed by one line of description). Follow this checklist when taking screenshots — 13 shots in total, satisfying the 10+ requirement.

1. Main Calculator Interface

Main interface


)(

img


)
Initial view: calculator on the left (display + 16 keys), history panel on the right.

2. Addition 12+8

Addition

(

img


)
Type 12+8 and press =; the display shows 20. The result is computed by the back end.

3. Subtraction 10-4

Subtraction

(

img


)

4. Multiplication 5*8

Multiplication

(

img


)
The button shows ×, while the API layer sends *.

5. Division and Decimals 10/4

Division

(

img


)
Result: 2.5.

6. Compound Expression and Precedence 1+2*3

Precedence

(

img


)
Correctly returns 7 instead of 9 — multiplication/division take precedence over addition/subtraction, guaranteed by the back-end parser.

7. Parentheses (1+2)*3

Parentheses

(

img


)
Returns 9.

8. Unary Plus/Minus -5+8 and 3*-2

Unary sign

(

img


)
A leading minus sign and a minus sign following an operator are both handled correctly.

9. Invalid Expression Handling 1++

Invalid expression

()
The back end returns 400 with an error message, the front end shows it in red, and no invalid result is written to history.

10. Division by Zero 5÷0

Division by zero

(

img


)
The back end returns "Division by zero", and the front end displays the error message.

11. Calculation History

History

(

img


)
The history panel refreshes automatically after each successful calculation, showing expression, result, and time — data comes from the back-end database.

12. History Persistence Check

Persistence

(

img


)
After pressing F5 (or closing and reopening the browser) the history is still there — because it is stored in the back-end SQLite database, not in any front-end cache.

13. Deleting One Record + Clearing All

Delete

(

img


)(

img


)
Click ✕ to delete a specific record, or "Clear All" to empty all history in the database; the front end refreshes according to the latest database state.

14. Extended Features

Extensions

(

img


)
Keyboard input (digits / operators / Enter / Backspace / Esc), dark theme switching, history search, and clicking a history item to refill the expression.


5. Design and Implementation Process

5.1 Requirement Analysis

RequirementDescriptionOwner
Basic arithmetic+ − × ÷; results must be computed by the back endBack-end calculation + front-end display
Compound expressionsPrecedence, parentheses, unary signs, decimalsBack-end parser
Exception handlingInvalid expressions, division by zero, illegal charactersBack-end validation + front-end messages
Calculation historyExpression / result / time stored in the back-end databaseBack-end database
Delete historyDelete one record by id; clearing all as an extensionBack-end API + front-end actions
Separated architectureNo calculation logic in the front end; API communicationArchitectural constraint

5.2 Overall Architecture

┌─────────────────────────┐        HTTP/JSON         ┌─────────────────────────┐
│      Front End (Web)     │  ──────────────────────▶ │     Back End (Flask)     │
│  index.html             │   POST /api/calculate    │  controller layer        │
│  css/style.css          │ ◀──────────────────────  │     ↓ calls              │
│  js/app.js (fetch)      │   200 {result: 9}        │  service layer           │
│                         │                          │   parsing / calc / history│
│  Interaction & display   │                          │     ↓ reads / writes     │
│  only — no calc logic    │                          │  model layer             │
└─────────────────────────┘                          │     ↓                   │
                                                     │  SQLite calculator.db   │
                                                     └─────────────────────────┘

5.3 Functional Structure Diagram

Calculator System
|
|-- Calculator
|   |-- Basic Calculation
|   |-- Compound Expression (precedence / parentheses / unary sign / decimals)
|   |-- Error Handling (invalid expression / division by zero)
|
|-- Calculation History
|   |-- Save History (auto-saved after each successful calculation)
|   |-- Query History (list + search filter)
|   |-- Delete History (single delete / clear all)
|
|-- Front End
|   |-- User Input (buttons + keyboard)
|   |-- Result & Error Display
|   |-- History Display & Search
|   |-- Theme Switching
|
|-- Back End
    |-- API (controller layer: routing / validation / unified responses)
    |-- Expression Processing (tokenization + recursive-descent parsing)
    |-- Calculation Service (evaluation + result formatting)
    |-- History Service (history business logic)
    |-- Database (SQLite persistence)

5.4 API Design

MethodPathDescriptionSuccessFailure
POST/api/calculateCalculate an expression and store it in history200400
GET/api/historyQuery all history200-
DELETE/api/history/{id}Delete a specific record200404
DELETE/api/historyClear all history (extension)200-

Unified response format (success carries success: true; failure carries success: false and a message):

// POST /api/calculate  request
{"expression": "(1+2)*3"}

// success 200
{"success": true, "expression": "(1+2)*3", "result": 9, "id": 12, "time": "2026-10-06 15:48:39"}

// failure 400
{"success": false, "message": "Division by zero"}

Status code design: missing parameters / syntax errors / division by zero → 400; deleting a non-existent record → 404; unknown path → 404; method not allowed → 405.

5.5 Database Design

The database is SQLite: zero configuration, a single file, bundled with the Python standard library — fully sufficient for this project's persistence needs.

Table calculation_history:

FieldTypeConstraintDescription
idINTEGERPRIMARY KEY AUTOINCREMENTUnique record id, used by the front end for deletion
expressionTEXTNOT NULLThe expression, e.g. (1+2)*3
resultTEXTNOT NULLDisplay string of the result, e.g. 9, 0.3
created_atTEXTNOT NULLCalculation time YYYY-MM-DD HH:MM:SS

Design notes:

  • result is stored as TEXT instead of REAL: reading 12+8 back from a REAL column would give 20.0, while we already format results into the most display-friendly string during calculation (integers without a decimal point). Storing TEXT guarantees "what is stored is what is displayed";
  • The time is generated by the back end at insert time, formatted to seconds;
  • CREATE TABLE IF NOT EXISTS runs at startup to create the table automatically; deleting the calculator.db file resets the database.

5.6 Calculation Module Design (Core)

Why not eval? eval("(1+2)*3") computes correctly, but a user input like __import__('os').system(...) would be executed as Python code — an arbitrary code execution vulnerability. The assignment also explicitly forbids it. Therefore I implemented my own parser:

  1. Tokenization: split the string into a token stream. Numbers (including decimals) become floats; operators/parentheses stay as characters; illegal characters raise an error immediately.
  2. Recursive-descent parsing + evaluation. The grammar has three layers; precedence falls out of the layering naturally:
expression := term  (('+' | '-') term)*      ← addition/subtraction, binds last
term       := factor (('*' | '/') factor)*   ← multiplication/division, binds first (higher precedence)
factor     := ('+' | '-') factor             ← unary plus/minus
            | '(' expression ')'             ← parentheses (recursion back to expression)
            | NUMBER
  • 1+2*3: expression first takes term 1, sees +, then takes another term — inside that term, 2*3 is fully multiplied out first, so precedence is achieved naturally;
  • -5+8, 3*-2: the factor layer handles unary -, allowing a minus at the start and after an operator;
  • (1+2)*3: when factor meets ( it recursively calls expression, computes 3, and continues with it as the multiplicand;
  • 5/0: the term layer checks the right operand before dividing and raises DivisionByZeroError;
  • Incomplete syntax (e.g. 1++, (1+2) raises ExpressionError at the corresponding layer, producing a 400.
  1. Result formatting: round(x, 10) removes floating-point tails (0.1+0.2 → 0.3 instead of 0.30000000000000004), and integer values drop the decimal point (20.0 → 20).

5.7 History Module Design

  • Write: after a successful calculation, the controller calls history_service → the database layer performs a parameterized INSERT (preventing SQL injection);
  • Query: SELECT ... ORDER BY id DESC, newest first; the front end only does display-level filtering (search) locally — the data source is always the database;
  • Delete: DELETE WHERE id = ?; if rowcount is 0 the API returns 404; after a successful deletion the front end re-queries the history API so the list always matches the database;
  • Persistence check: history exists only in the back-end database, so refreshing, restarting, or switching browsers never loses it.

5.8 Exception Handling Design

LayerExceptionHandling
controllerNon-JSON body / missing expression field / overlong inputDirect 400 + message
serviceExpressionError (syntax / illegal characters)Caught and returned as 400; nothing written to history
serviceDivisionByZeroErrorCaught and returned as 400 "Division by zero"
Global404 / 405 / 500Flask errorhandler returning unified JSON

Principle: only successful calculations enter the database; every failure returns structured JSON, and the front end uses the success field to decide how to display it.

5.9 Front-End/Back-End Interaction Flow

User presses keys → front end builds the expression (display layer uses × ÷)
Press = → front end converts × ÷ to * / → POST /api/calculate {"expression":"(1+2)*3"}
     → back-end controller validates parameters
     → service: tokenization + recursive-descent evaluation
     → success: format result → INSERT into database → return 200 {result:9}
     → failure: return 400 {message}
Front end receives response → success=true shows "= 9" and refreshes history
            → success=false shows the message in red
            → data is null (network down) shows "cannot reach the back-end service"

One implementation detail: with fetch you must not rely on the HTTP status code alone — when the back end returns 400 the response body is still JSON, so the front end must parse it and use the success field to distinguish a "business error" from a "network failure".

5.10 Deployment Process

  • Back end: deployed on a free PythonAnywhere account (Python 3.10 virtualenv, pip install flask, no credit card required); PythonAnywhere's WSGI config loads the app object from src/app.py, and the SQLite file persists inside the project directory, so history survives refreshes and restarts;
  • Front end: static pages deployed on GitHub Pages (index.html sits at the repository root); simply change API_BASE_URL at the top of js/app.js to the back end's public address — the back end already enables CORS;
  • Local demo: python src/app.py starts the back end (port 5000) + python -m http.server 5500 serves the front end.

6. Code Explanation

6.1 Back End: Expression Parser (Tokenization + Recursive Descent)

Full code in src/service/expression_parser.py; the three snippets below best illustrate the design:

def tokenize(expression):
    """Lexical analysis: numbers become floats, operators stay as
    characters, illegal characters raise an error immediately."""
    tokens = []
    i = 0
    while i < len(expression):
        char = expression[i]
        if char in _OPERATORS:                # + - * / ( )
            tokens.append(char); i += 1
        elif char.isdigit() or char == ".":
            # read the whole number (including decimal point);
            # two decimal points are illegal
            ...
            tokens.append(float(number_text))
        else:
            raise ExpressionError("Invalid character: " + char)
    return tokens

Why this design: the lexical layer first turns a "character stream" into a "token stream", so the syntax layer only deals with clean tokens — validation responsibilities are cleanly separated: illegal characters are caught at the lexical layer, syntax errors at the parsing layer.

def _expression(self):
    """Addition/subtraction layer: term (('+'|'-') term)*"""
    value = self._term()
    while self._peek() in ("+", "-"):
        operator = self._advance()
        right = self._term()          # ← key: the right operand is a whole term
        value = value + right if operator == "+" else value - right
    return value

def _term(self):
    """Multiplication/division layer: factor (('*'|'/') factor)*;
    division by zero is caught here."""
    value = self._factor()
    while self._peek() in ("*", "/"):
        operator = self._advance()
        right = self._factor()
        if operator == "*":
            value = value * right
        else:
            if right == 0:
                raise DivisionByZeroError("Division by zero")
            value = value / right
    return value

Why this design: the right operand in _expression is _term(), and _term fully evaluates all multiplications/divisions first — precedence needs no extra markers because the grammar layers guarantee it. Division by zero is checked before dividing, avoiding a bare Python ZeroDivisionError turning into a 500.

def _factor(self):
    """Unary sign / parentheses / number."""
    token = self._peek()
    if token == "-":
        self._advance()
        return -self._factor()        # recursion supports 3*-2, --5, etc.
    if token == "(":
        self._advance()
        value = self._expression()    # inside parentheses, recurse to the top layer
        if self._peek() != ")":
            raise ExpressionError("Missing closing parenthesis")
        self._advance()
        return value
    if isinstance(token, float):
        return self._advance()
    raise ExpressionError("Incomplete expression")   # e.g. "1++" missing a number

6.2 Back End: Calculation Endpoint (Controller Layer)

@api_bp.route("/calculate", methods=["POST"])
def calculate():
    body = request.get_json(silent=True)          # bad JSON → None, not a 500
    if body is None:
        return _error("Request body must be valid JSON", 400)
    raw_expression = body.get("expression")
    if not isinstance(raw_expression, str):       # never trust front-end data
        return _error("Field 'expression' is required...", 400)
    try:
        outcome = calculator_service.calculate_and_store(raw_expression)
    except calculator_service.expression_parser.DivisionByZeroError:
        return _error("Division by zero", 400)
    except calculator_service.expression_parser.ExpressionError as exc:
        return _error(str(exc), 400)
    return jsonify({...,"result": outcome["result"], ...}), 200

Why this design: the controller only does three things — "validate parameters → call the service → translate business exceptions into HTTP status codes"; all calculation logic lives in the service layer. Business exceptions (syntax errors, division by zero) are uniformly converted to 400 at this try/except boundary, so user input can never cause a 500.

6.3 Back End: Database Layer (Parameterized SQL)

def insert_history(expression, result):
    created_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    with get_connection() as conn:
        cursor = conn.execute(
            "INSERT INTO calculation_history (expression, result, created_at) "
            "VALUES (?, ?, ?)",                     # parameterized query, prevents SQL injection
            (expression, result, created_at),
        )
        return cursor.lastrowid, created_at

6.4 Front End: API Request Wrapper

async function requestApi(path, options = {}) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
  try {
    const response = await fetch(API_BASE_URL + path, {
      ...options, signal: controller.signal,
    });
    const data = await response.json().catch(() => null);
    return { ok: response.ok, status: response.status, data };
  } catch (networkError) {
    return { ok: false, status: 0, data: null };  // all network failures land here
  } finally {
    clearTimeout(timer);                          // clear the timer either way
  }
}

Why this design: every request goes through one wrapper with a unified 8-second timeout (the front end never hangs forever when the back end is down), and all network failures collapse into data: null, so business code only needs to check data.success.

6.5 Front End: Calculation Flow (Reflecting the Separated Architecture)

async function handleEquals() {
  // convert display-layer × ÷ into API-layer * /
  const expressionToSend = currentExpression.replace(/×/g, '*').replace(/÷/g, '/');
  const { data } = await apiCalculate(expressionToSend);   // ← sends the expression only
  if (!data) { showMessage('Cannot reach the back-end service…'); return; }  // ← back end down → no result
  if (data.success) {
    renderResult('= ' + data.result);                      // ← shows the back end's result
    loadHistory();                                         // ← refreshes history (from the database)
  } else {
    showMessage(data.message);                             // ← shows the back end's error
  }
}

Note: there is no calculation code anywhere in the front end — data.result comes entirely from the back end. If the back-end service is stopped, this function always ends at the "cannot reach the back-end service" branch — exactly the verification method the assignment requires.

6.6 Front End: Re-query After Deleting History

deleteBtn.addEventListener('click', async (event) => {
  event.stopPropagation();
  const { data } = await apiDeleteHistory(record.id);  // DELETE /api/history/{id}
  if (data && data.success) {
    loadHistory();   // not removed locally — re-query the database's latest state
  } else {
    showMessage((data && data.message) || 'Delete failed');
  }
});

Why this design: after deleting, the front end re-runs GET /api/history instead of just removing the record locally, guaranteeing the list always reflects the database's real state (explicitly required by the assignment).


7. Personal Journey and Learnings

  1. Understanding front-end/back-end separation: I used to think "separation" just meant putting code in two folders. Only after finishing did I understand the core is separation of responsibilities — the front end does interaction and display only, while calculation, validation, and storage all live on the back end. Verifying the architecture by "stopping the back end and confirming the front end can no longer compute" is a very intuitive check.
  2. Expression parsing was the hardest part: I first wanted to take the shortcut of eval, but research showed it is an arbitrary code execution vulnerability (an input like __import__("os") gives control of the machine). After switching to a recursive-descent parser, I truly understood that "grammar layering determines precedence": the addition layer calls the multiplication layer, so multiplication naturally computes first.
  3. Floating-point traps: 0.1+0.2 = 0.30000000000000004 is the classic IEEE 754 problem; I solved it with display-layer formatting — rounding to 10 decimal places and trimming integers.
  4. Integration pitfalls: the most typical were CORS and error judgment — when the back end returns 400, fetch's response.ok is false, which I first mistook for a network error showing "cannot reach the back-end service". After debugging, I switched to parsing the response body's success field to distinguish business errors from network errors.
  5. Database design trade-off: the result column was originally REAL, which made 12+8 display as 20.0; I changed it to store the formatted string so "what is stored is what is displayed".
  6. Possible improvements: the free PythonAnywhere instance is periodically recycled and needs a periodic login to stay alive, so migrating to an always-on cloud server would be more robust; history queries have no pagination and should add LIMIT/OFFSET for large datasets; the parser currently supports only the four basic operations and could be extended with powers, functions, and other scientific-calculation capabilities.

Appendix: Running Locally

# Back end (defaults to 127.0.0.1:5000)
cd 832402212_calculator_backend
pip install -r requirements.txt
python src/app.py

# Front end (in another terminal)
cd 832402212_calculator_frontend
python -m http.server 5500
# Open http://localhost:5500 in the browser
...全文
139 回复 打赏 收藏 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

163

社区成员

发帖
与我相关
我的任务
社区描述
梅努斯-软件工程
软件工程 高校 福建省·福州市
社区管理员
  • FZU_SE_LQF
  • zjcTander
  • 木村修
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧