Building a Web-Based Restaurant Ordering System Using Java, Vue.js, and MyBatis

Technical Architecture & Stack

The ordering platform follows a decoupled three-tier architecture designed for academic and prototyping purposes. The client layer utilizes vanilla JavaScript combined with Vue.js for reactive state management and Element UI for rapid interface compositoin. Data transmission is handled via Axios HTTP requests targeting a Java EE backend. The server layer relies on standard Servlets for request routing and business orchestration, while MyBatis serves as the persistence framework for SQL mapping. MySQL is deployed as the relational database, managed through visual administration tools.

Core Functional Modules

  • User Authentication & Registration: Handles credential validation, session tracking, and role assignment (Admin vs. Customer).
  • Dish Catalog Management: Enables administrators to perform CRUD operations on menu items, including status toggling (available/sold out) and bulk actions.
  • Order Processing: Allows customers to select dishes, adjust quantities, simulate payment gateways, and submit purchase requests.
  • Order Tracking & History: Provides users with a viewable list of past transactions and administrative oversight of incoming orders.
  • Profile Configuration: Secure editing of shipping addresses, contact numbers, and authentication credentials.

Note: While initial specifications included branch location routing and logistics tracking, these were excluded from the MVP build to prioritize core transactional flows.

Database Schema Design

The relational model centers around three primary entities to maintain normalization while supporting rapid query execution:

  • tb_user: Stores account credentials, roles, delivery coordinates, and phone numbers.
  • tb_goods: Manages product metadata, pricing, merchant attribution, stock availability flags, and descriptions.
  • tb_order: Links users to purchased goods, records monetary totals, selection modes, and timestamps.

Frontend Implementation with Vue.js & Element UI

Instead of duplicating near-identical HTML-Vue templates across separate files, modern practice consolidates reusable components. Below is a refactored demonstration of the catalog management interface. This structure replaces the original verbose layout, introduces clearer state management, and aliases internal variables for better readability and maintenance.


<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Menu Administration</title>
  <link rel="stylesheet" href="assets/theme/index.css">
  <style>
    .item-row:nth-child(3n) { background-color: #fdf6ec; }
    .item-row:nth-child(3n+1) { background-color: #f0f9eb; }
  </style>
</head>
<body>
  <div id="catalog-app">
    <el-menu mode="horizontal" background-color="#304156" text-color="#bfcbd9">
      <el-menu-item index="1">Dashboard</el-menu-item>
      <el-menu-item index="2">Menu Control</el-menu-item>
      <el-menu-item index="3">Transaction View</el-menu-item>
      <el-menu-item index="4" @click="logout">Sign Out</el-menu-item>
    </el-menu>

    <el-form :model="searchFilters" inline class="toolbar">
      <el-form-item label="Availability">
        <el-select v-model="searchFilters.isAvailable">
          <el-option label="Ready to Order" value="1"></el-option>
          <el-option label="Out of Stock" value="0"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item label="Item Name"><el-input v-model="searchFilters.name"></el-input></el-form-item>
      <el-form-item>
        <el-button type="primary" @click="refreshGrid">Apply Filters</el-button>
        <el-button type="info" plain @click="resetFilters">Clear</el-button>
      </el-form-item>
    </el-form>

    <div class="actions-bar">
      <el-button type="danger" plain @click="handleBulkRemove">Delete Selected</el-button>
      <el-button type="success" @click="openCreateModal">Add New Dish</el-button>
    </div>

    <el-dialog title="Dish Creation" :visible.sync="isFormModalOpen" width="35%">
      <el-form :model="entryPayload" label-width="100px">
        <el-form-item label="Dish Title"><el-input v-model="entryPayload.title"></el-input></el-form-item>
        <el-form-item label="Merchant"><el-input v-model="entryPayload.vendor"></el-input></el-form-item>
        <el-form-item label="Cost"><el-input-number v-model="entryPayload.cost"></el-input-number></el-form-item>
        <el-form-item label="Status">
          <el-switch v-model="entryPayload.isAvailable" active-value="1" inactive-value="0"></el-switch>
        </el-form-item>
        <el-form-item><el-button type="primary" @click="submitNewRecord">Save</el-button><el-button @click="closeModal">Cancel</el-button></el-form-item>
      </el-form>
    </el-dialog>

    <el-table :data="gridDataSource" border stripe row-class-name="item-row">
      <el-table-column type="selection" width="50" align="center"></el-table-column>
      <el-table-column prop="id" label="ID" width="70" align="center"></el-table-column>
      <el-table-column prop="title" label="Title" align="center"></el-table-column>
      <el-table-column prop="vendor" label="Vendor" align="center"></el-table-column>
      <el-table-column prop="cost" label="Price" align="center"></el-table-column>
      <el-table-column prop="statusLabel" label="State" align="center"></el-table-column>
      <el-table-column label="Actions" align="center">
        <template slot-scope="scope">
          <el-button size="mini" @click="prepareEdit(scope.row)">Edit</el-button>
          <el-button size="mini" type="danger" @click="removeSingle(scope.row.id)">Remove</el-button>
        </template>
      </el-table-column>
    </el-table>

    <el-pagination
      background
      layout="total, sizes, prev, pager, next"
      :page-sizes="[5, 10, 15]"
      :current-page="pagination.current"
      :page-size="pagination.size"
      :total="pagination.total"
      @size-change="handlePageSizeChange"
      @current-change="handlePageJump">
    </el-pagination>
  </div>

  <script src="libs/vue.min.js"></script>
  <script src="libs/axios.min.js"></script>
  <script src="assets/ui/index.js"></script>
  <script>
    new Vue({
      el: '#catalog-app',
      data() {
        return {
          isFormModalOpen: false,
          entryPayload: { id: null, title: '', vendor: '', cost: 0, isAvailable: '1' },
          searchFilters: { name: '', isAvailable: '' },
          gridDataSource: [],
          selectedRows: [],
          pagination: { current: 1, size: 10, total: 0 }
        };
      },
      created() { this.loadCatalog(); },
      methods: {
        loadCatalog() {
          const params = new URLSearchParams({
            cp: this.pagination.current, ps: this.pagination.size,
            n: this.searchFilters.name, s: this.searchFilters.isAvailable
          });
          axios.post('/api/menu/list?' + params.toString())
            .then(res => {
              this.gridDataSource = res.data.items;
              this.pagination.total = res.data.count;
            })
            .catch(err => console.error('Fetch failed:', err));
        },
        handlePageSizeChange(val) { this.pagination.size = val; this.loadCatalog(); },
        handlePageJump(val) { this.pagination.current = val; this.loadCatalog(); },
        openCreateModal() { this.entryPayload = { id: null, title: '', vendor: '', cost: 0, isAvailable: '1' }; this.isFormModalOpen = true; },
        closeModal() { this.isFormModalOpen = false; },
        submitNewRecord() {
          axios.post('/api/menu/create', this.entryPayload)
            .then(() => { this.closeModal(); this.$message.success('Record added'); this.loadCatalog(); });
        },
        prepareEdit(row) {
          this.entryPayload = { ...row };
          this.isFormModalOpen = true;
        },
        removeSingle(id) {
          this.$confirm('Permanently delete this item?', 'Confirm', { type: 'warning' })
            .then(() => {
              axios.get(`/api/menu/remove/${id}`)
                .then(() => { this.$message.success('Deleted'); this.loadCatalog(); });
            }).catch(() => {});
        },
        handleBulkRemove() {
          if (!this.selectedRows.length) return this.$message.warning('Select at least one row');
          const ids = this.selectedRows.map(r => r.id);
          this.$confirm('Proceed with batch deletion?', 'Warning', { type: 'warning' })
            .then(() => {
              axios.post('/api/menu/batch-delete', ids)
                .then(() => { this.$message.success('Batch removal complete'); this.loadCatalog(); });
            }).catch(() => {});
        },
        logout() { window.location.href = '/auth/signout'; }
      }
    });
  </script>
</body>
</html>

Persistence & Dependency Configuration

Build automation and ORM setup are streamlined through standardized configuration files. The Maven descriptor aggregates runtime scopes correctly, isolating test frameworks and specifying the Servlet API as provided by the container.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.academy.foodsys</groupId>
  <artifactId>restaurant-webapp</artifactId>
  <version>2.0.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <mybatis.version>3.5.13</mybatis.version>
    <jackson.version>2.15.2</jackson.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>4.0.1</version>
      <scope>provided</scope>
    </dependency>

    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>${mybatis.version}</version>
    </dependency>

    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.33</version>
    </dependency>

    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>${jackson.version}</version>
    </dependency>

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.13.2</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.tomcat.maven</groupId>
        <artifactId>tomcat7-maven-plugin</artifactId>
        <version>2.2</version>
        <configuration>
          <port>9090</port>
          <path>/</path>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Database connectivity and object-relational mapping boundaries are centralized in the MyBatis bootstrap file. Type aliases simplify mapper XML queries, while connection pooling properties enforce standard JDBC parameters.

<?xml version="1.0" encoding="UTF-8" ?>

<configuration>
  <settings>
    <setting name="logImpl" value="STDOUT_LOGGING"/>
  </settings>

  <typeAliases>
    <package name="com.restaurant.db.entities"/>
  </typeAliases>

  <environments default="production">
    <environment id="production">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/dining_db?useSSL=false&serverTimezone=UTC&characterEncoding=utf8"/>
        <property name="username" value="db_admin"/>
        <property name="password" value="secure_pass_123"/>
      </dataSource>
    </environment>
  </environments>

  <mappers>
    <package name="com.restaurant.db.mappers"/>
  </mappers>
</configuration>

The remaining client views (authentication flows, customer checkout pipelines, transaction history dashboards, and profile editing forms) adhere to the structural patterns established above. Backend controllers route requests through DTO validators, delegate to service layers, and serialize JSON responses via Jackson parsers. Session tokens verify role-based access control before executing administrative mutations.

Tags: java-servlets vue-js element-ui mybatis-framework mysql-database

Posted on Sat, 31 Oct 2026 16:45:04 +0000 by ianr