Skip to content

S09-08 MySQL-项目:满汉楼 ​

[TOC]

项目总览 ​

系统定位 ​

满汉楼餐饮管理系统是 Java SE 阶段通向企业级开发的经典综合实战项目。系统模拟真实餐厅的日常运营全流程,核心目的在于帮助开发者建立面向对象分层架构思维,掌握基于 Druid 连接池与 DBUtils 泛型反射 的通用 CRUD 设计。

text
[员工登录] ──> [查看餐桌] ──> [客户预订] ──> [开台点餐] ──> [明细核对] ──> [结账买单/清台]

技术选型 ​

技术维度选型组件核心作用
编程语言Java SE (JDK 8+)面向对象设计、集合框架、泛型与反射机制
数据存储MySQL 5.7+ (InnoDB)关系型数据存储、ACID 事务与行级锁
连接池Alibaba Druid数据库连接的高效复用与生命周期管理
持久层工具Apache Commons DBUtils简化 JDBC 代码,自动完成 SQL 结果集到 JavaBean 的 ORM 映射
数据安全MD5 散列摘要员工密码脱敏加密存储与校验

工程目录结构 ​

text
com.mhl/
├── domain/       # 实体层:与数据库表一一对应的 JavaBean 及多表 DTO
├── dao/          # 持久层:BasicDAO 通用父类及各业务 DAO 实现
├── service/      # 业务层:状态流转、业务校验与规则调度
├── view/         # 表现层:控制台多级循环菜单与人机交互
└── utils/        # 工具层:Druid 连接池管理、安全键盘输入工具

基础底座 ​

数据库连接池 JDBCUtilsByDruid ​

通过静态代码块解析 druid.properties 配置文件,构建全局唯一的连接池,提供安全的连接获取与资源释放机制。

properties
# src/druid.properties
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/mhl?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username=root
password=root
initialSize=10
maxActive=30
maxWait=5000
minIdle=5
java
package com.mhl.utils;

import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

public class JDBCUtilsByDruid {
  private static DataSource ds;

  static {
    Properties properties = new Properties();
    try {
      InputStream in = JDBCUtilsByDruid.class.getClassLoader().getResourceAsStream("druid.properties");
      properties.load(in);
      ds = DruidDataSourceFactory.createDataSource(properties);
    } catch (Exception e) {
      throw new RuntimeException("Druid 连接池初始化失败", e);
    }
  }

  public static Connection getConnection() throws SQLException {
    return ds.getConnection();
  }

  public static void close(ResultSet rs, Statement stmt, Connection conn) {
    try {
      if (rs != null) rs.close();
      if (stmt != null) stmt.close();
      if (conn != null) conn.close(); // 归还连接至池中
    } catch (SQLException e) {
      throw new RuntimeException(e);
    }
  }
}

通用泛型持久层 BasicDAO<T> ​

利用 Apache Commons DBUtils 的 QueryRunner 与 Java 泛型反射(Class<T>),实现所有业务 DAO 零样板代码的单行、多行及标量查询。

java
package com.mhl.dao;

import com.mhl.utils.JDBCUtilsByDruid;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;

public class BasicDAO<T> {
  private QueryRunner qr = new QueryRunner();

  // 通用 DML (INSERT, UPDATE, DELETE)
  public int update(String sql, Object... params) {
    Connection conn = null;
    try {
      conn = JDBCUtilsByDruid.getConnection();
      return qr.update(conn, sql, params);
    } catch (SQLException e) {
      throw new RuntimeException("执行 DML 失败: " + sql, e);
    } finally {
      JDBCUtilsByDruid.close(null, null, conn);
    }
  }

  // 查询返回多行 JavaBean 集合
  public List<T> queryMulti(String sql, Class<T> clazz, Object... params) {
    Connection conn = null;
    try {
      conn = JDBCUtilsByDruid.getConnection();
      return qr.query(conn, sql, new BeanListHandler<>(clazz), params);
    } catch (SQLException e) {
      throw new RuntimeException("执行多行查询失败: " + sql, e);
    } finally {
      JDBCUtilsByDruid.close(null, null, conn);
    }
  }

  // 查询返回单行 JavaBean 对象
  public T querySingle(String sql, Class<T> clazz, Object... params) {
    Connection conn = null;
    try {
      conn = JDBCUtilsByDruid.getConnection();
      return qr.query(conn, sql, new BeanHandler<>(clazz), params);
    } catch (SQLException e) {
      throw new RuntimeException("执行单行查询失败: " + sql, e);
    } finally {
      JDBCUtilsByDruid.close(null, null, conn);
    }
  }

  // 查询返回单行单列的标量值 (COUNT/SUM/MAX等)
  public Object queryScalar(String sql, Object... params) {
    Connection conn = null;
    try {
      conn = JDBCUtilsByDruid.getConnection();
      return qr.query(conn, sql, new ScalarHandler<>(), params);
    } catch (SQLException e) {
      throw new RuntimeException("执行标量查询失败: " + sql, e);
    } finally {
      JDBCUtilsByDruid.close(null, null, conn);
    }
  }
}

主程序 ​

java
package com.mhl.view;

import com.mhl.domain.*;
import com.mhl.service.*;
import com.mhl.utils.Utility;

import java.util.List;

public class MHLView {
  private boolean loop = true;
  private EmployeeService employeeService = new EmployeeService();
  private DiningTableService diningTableService = new DiningTableService();
  private MenuService menuService = new MenuService();
  private BillService billService = new BillService();

  public static void main(String[] args) {
    new MHLView().mainMenu();
  }

  public void mainMenu() {
    while (loop) {
      System.out.println("\n================= 满汉楼网络餐饮系统 =================");
      System.out.println("		 1 登录满汉楼");
      System.out.println("		 2 退出系统");
      System.out.print("请输入你的选择: ");
      String key = Utility.readString(1);

      switch (key) {
        case "1":
          System.out.print("请输入员工号: ");
          String empId = Utility.readString(50);
          System.out.print("请输入密  码: ");
          String pwd = Utility.readString(50);
          Employee emp = employeeService.getEmployeeByIdAndPwd(empId, pwd);
          if (emp != null) {
            System.out.println("\n============ 登录成功 [" + emp.getName() + " - " + emp.getJob() + "] ============");
            secondaryMenu();
          } else {
            System.out.println("========== 登录失败:工号或密码错误 ==========");
          }
          break;
        case "2":
          loop = false;
          System.out.println("退出满汉楼系统成功!");
          break;
        default:
          System.out.println("输入有误,请重新输入!");
      }
    }
  }

  private void secondaryMenu() {
    boolean secLoop = true;
    while (secLoop) {
      System.out.println("================= 满汉楼二级主菜单 =================");
      System.out.println("		 1 显示餐桌状态");
      System.out.println("		 2 预订餐桌");
      System.out.println("		 3 显示所有菜品");
      System.out.println("		 4 点餐服务");
      System.out.println("		 5 查看账单");
      System.out.println("		 6 结账");
      System.out.println("		 9 退出系统");
      System.out.print("请输入你的选择: ");
      String key = Utility.readString(1);

      switch (key) {
        case "1":
          showDiningTable();
          break;
        case "2":
          orderDiningTable();
          break;
        case "3":
          showMenu();
          break;
        case "4":
          orderMenu();
          break;
        case "5":
          showBill();
          break;
        case "6":
          payBill();
          break;
        case "9":
          secLoop = false;
          break;
        default:
          System.out.println("输入指令不存在,请重新输入!");
      }
    }
  }

  private void showDiningTable() {
    List<DiningTable> list = diningTableService.list();
    System.out.println("餐桌编号	餐桌状态	预订人	预订电话");
    for (DiningTable dt : list) {
      System.out.println(dt.getId() + "		" + dt.getState() + "		" + dt.getOrderName() + "		" + dt.getOrderTel());
    }
  }

  private void orderDiningTable() {
    System.out.println("\n================= 预订餐桌 =================");
    System.out.print("请选择预订的餐桌编号(-1退出): ");
    int orderId = Utility.readInt();
    if (orderId == -1) return;

    char key = Utility.readConfirmSelection();
    if (key == 'Y') {
      DiningTable dt = diningTableService.getDiningTableById(orderId);
      if (dt == null) {
        System.out.println("该餐桌不存在!");
        return;
      }
      if (!"空闲".equals(dt.getState())) {
        System.out.println("该餐桌已被预订或正在就餐中!");
        return;
      }
      System.out.print("预订人姓名: ");
      String orderName = Utility.readString(50);
      System.out.print("预订人电话: ");
      String orderTel = Utility.readString(20);
      if (diningTableService.orderDiningTable(orderId, orderName, orderTel)) {
        System.out.println("============= 预订餐桌成功 =============");
      }
    }
  }

  private void showMenu() {
    List<Menu> list = menuService.list();
    System.out.println("菜品编号	菜品名		类别		价格");
    for (Menu menu : list) {
      System.out.println(menu.getId() + "		" + menu.getName() + "		" + menu.getType() + "		" + menu.getPrice());
    }
  }

  private void orderMenu() {
    System.out.println("\n================= 点餐服务 =================");
    System.out.print("请输入点餐的桌号(-1退出): ");
    int tableId = Utility.readInt();
    if (tableId == -1) return;
    DiningTable dt = diningTableService.getDiningTableById(tableId);
    if (dt == null) {
      System.out.println("餐桌号不存在!");
      return;
    }

    System.out.print("请输入菜品编号(-1退出): ");
    int menuId = Utility.readInt();
    if (menuId == -1) return;
    Menu menu = menuService.getMenuById(menuId);
    if (menu == null) {
      System.out.println("菜品编号不存在!");
      return;
    }

    System.out.print("请输入点餐份数(-1退出): ");
    int nums = Utility.readInt();
    if (nums == -1) return;

    if (billService.orderMenu(menuId, nums, tableId)) {
      System.out.println("============= 点餐成功并更新餐桌状态 =============");
    }
  }

  private void payBill() {
    System.out.println("\n================= 结账买单 =================");
    System.out.print("请选择结账的餐桌编号(-1退出): ");
    int tableId = Utility.readInt();
    if (tableId == -1) return;
    DiningTable dt = diningTableService.getDiningTableById(tableId);
    if (dt == null) {
      System.out.println("结账餐桌不存在!");
      return;
    }
    if (!billService.hasUnpaidBill(tableId)) {
      System.out.println("该餐桌没有未结账单!");
      return;
    }

    System.out.print("结账方式 (现金/微信/支付宝) 回车退出: ");
    String payMode = Utility.readString(20, "");
    if ("".equals(payMode)) return;

    char confirm = Utility.readConfirmSelection();
    if (confirm == 'Y') {
      if (billService.payBill(tableId, payMode)) {
        System.out.println("============= 结账成功,已释放餐桌 =============");
      }
    }
  }

  private void showBill() {
    List<MultiTableBean> multiTableBeans = billService.listMultiTable();
    System.out.println("\n编号	菜品号	菜名		单价	份数	总金额	桌号	日期				状态");
    for (MultiTableBean b : multiTableBeans) {
      System.out.println(b.getId() + "	" + b.getMenuId() + "		" + b.getName() + "		" +
          b.getPrice() + "	" + b.getNums() + "	" + b.getMoney() + "	" +
          b.getDiningTableId() + "	" + b.getBillDate() + "	" + b.getState());
    }
  }
}

员工登录 ​

业务流程与表设计 ​

系统一级入口。员工输入工号和明文密码,服务端通过 MD5 散列匹配数据库,验证通过后开放二级主操作菜单。

image-20260831143626914

字段名类型约束业务说明
idINTPRIMARY KEY, AUTO_INCREMENT员工唯一 ID
empIdVARCHAR(50)UNIQUE, NOT NULL员工工号(如 "6666")
pwdCHAR(32)NOT NULL登录密码(MD5 密文存储)
nameVARCHAR(50)NOT NULL员工姓名
jobVARCHAR(50)NOT NULL岗位(大堂经理/服务员/前台)

实体与持久层 ​

java
// 实体类 Employee.java
package com.mhl.domain;

public class Employee {
  private Integer id;
  private String empId;
  private String pwd;
  private String name;
  private String job;

  public Employee() {}
  // Getters and Setters...
  public Integer getId() { return id; }
  public void setId(Integer id) { this.id = id; }
  public String getEmpId() { return empId; }
  public void setEmpId(String empId) { this.empId = empId; }
  public String getPwd() { return pwd; }
  public void setPwd(String pwd) { this.pwd = pwd; }
  public String getName() { return name; }
  public void setName(String name) { this.name = name; }
  public String getJob() { return job; }
  public void setJob(String job) { this.job = job; }
}
java
// 数据访问 EmployeeDAO.java
package com.mhl.dao;
import com.mhl.domain.Employee;

public class EmployeeDAO extends BasicDAO<Employee> {}

鉴权业务与控制台交互 ​

java
// 业务层 EmployeeService.java
package com.mhl.service;

import com.mhl.dao.EmployeeDAO;
import com.mhl.domain.Employee;

public class EmployeeService {
  private EmployeeDAO employeeDAO = new EmployeeDAO();

  // 校验工号与密码 (SQL 中使用 MD5 函数直接比对)
  public Employee getEmployeeByIdAndPwd(String empId, String pwd) {
    String sql = "SELECT * FROM employee WHERE empId = ? AND pwd = MD5(?)";
    return employeeDAO.querySingle(sql, Employee.class, empId, pwd);
  }
}
  • 表现层交互逻辑:
    1. 用户在控制台选择 1 登录满汉楼;
    2. 调用 Utility.readString() 读取工号与密码;
    3. 调用 EmployeeService.getEmployeeByIdAndPwd(),校验成功显示欢迎信息并跳转二级主菜单,失败则提示重试。

餐桌预订 ​

业务流程与表设计 ​

餐桌是餐厅运营的核心实体,严格遵循 三态状态机 转移规则:

text
[空闲] ──(客户预订)──> [已预订] ──(开台点餐)──> [就餐中] ──(结账买单)──> [空闲 (清空预订人)]
字段名类型约束业务说明
idINTPRIMARY KEY, AUTO_INCREMENT餐桌物理编号 (1~N 号桌)
stateVARCHAR(20)DEFAULT '空闲'餐桌当前状态 (空闲/已预订/就餐中)
orderNameVARCHAR(50)DEFAULT ''预订人姓名
orderTelVARCHAR(20)DEFAULT ''预订人联系电话

实体与持久层 ​

java
// 实体类 DiningTable.java
package com.mhl.domain;

public class DiningTable {
  private Integer id;
  private String state;
  private String orderName;
  private String orderTel;

  public DiningTable() {}
  // Getters and Setters...
  public Integer getId() { return id; }
  public void setId(Integer id) { this.id = id; }
  public String getState() { return state; }
  public void setState(String state) { this.state = state; }
  public String getOrderName() { return orderName; }
  public void setOrderName(String orderName) { this.orderName = orderName; }
  public String getOrderTel() { return orderTel; }
  public void setOrderTel(String orderTel) { this.orderTel = orderTel; }
}
java
// 数据访问 DiningTableDAO.java
package com.mhl.dao;
import com.mhl.domain.DiningTable;

public class DiningTableDAO extends BasicDAO<DiningTable> {}

预订业务与控制台交互 ​

java
// 业务层 DiningTableService.java
package com.mhl.service;

import com.mhl.dao.DiningTableDAO;
import com.mhl.domain.DiningTable;
import java.util.List;

public class DiningTableService {
  private DiningTableDAO diningTableDAO = new DiningTableDAO();

  // 1. 查询所有餐桌列表
  public List<DiningTable> list() {
    return diningTableDAO.queryMulti("SELECT * FROM diningTable", DiningTable.class);
  }

  // 2. 根据 ID 获取餐桌详情
  public DiningTable getDiningTableById(int id) {
    return diningTableDAO.querySingle("SELECT * FROM diningTable WHERE id = ?", DiningTable.class, id);
  }

  // 3. 预订餐桌 (更新状态并登记客户信息)
  public boolean orderDiningTable(int id, String orderName, String orderTel) {
    String sql = "UPDATE diningTable SET state = '已预订', orderName = ?, orderTel = ? WHERE id = ?";
    return diningTableDAO.update(sql, orderName, orderTel, id) > 0;
  }

  // 4. 更新餐桌就餐状态
  public boolean updateDiningTableState(int id, String state) {
    String sql = "UPDATE diningTable SET state = ? WHERE id = ?";
    return diningTableDAO.update(sql, state, id) > 0;
  }

  // 5. 结账清台 (重置为空闲并清空客户信息)
  public boolean updateDiningTableToFree(int id) {
    String sql = "UPDATE diningTable SET state = '空闲', orderName = '', orderTel = '' WHERE id = ?";
    return diningTableDAO.update(sql, id) > 0;
  }
}
  • 表现层交互逻辑:
    • 显示餐桌:遍历 diningTableService.list(),控制台表格化输出。

      image-20260831143813105

    • 预订餐桌:输入桌号 → 校验该桌状态是否为 "空闲" → 输入姓名与手机号 → 确认后更新数据库。

      image-20260831150340917

菜谱管理 ​

业务流程与表设计 ​

维护门店在售菜品清单与单价,供点餐时选择与核价。

字段名类型约束业务说明
idINTPRIMARY KEY, AUTO_INCREMENT菜品编号
nameVARCHAR(50)NOT NULL菜品名称(如“佛跳墙”、“东坡肉”)
typeVARCHAR(50)NOT NULL菜品类别(热菜/汤品/主食)
priceDOUBLE(10,2)NOT NULL菜品单价 (元)

实体与持久层 ​

java
// 实体类 Menu.java
package com.mhl.domain;

public class Menu {
  private Integer id;
  private String name;
  private String type;
  private Double price;

  public Menu() {}
  // Getters and Setters...
  public Integer getId() { return id; }
  public void setId(Integer id) { this.id = id; }
  public String getName() { return name; }
  public void setName(String name) { this.name = name; }
  public String getType() { return type; }
  public void setType(String type) { this.type = type; }
  public Double getPrice() { return price; }
  public void setPrice(Double price) { this.price = price; }
}
java
// 数据访问 MenuDAO.java
package com.mhl.dao;
import com.mhl.domain.Menu;

public class MenuDAO extends BasicDAO<Menu> {}

菜谱查询与控制台交互 ​

java
// 业务层 MenuService.java
package com.mhl.service;

import com.mhl.dao.MenuDAO;
import com.mhl.domain.Menu;
import java.util.List;

public class MenuService {
  private MenuDAO menuDAO = new MenuDAO();

  // 获取全量菜品列表
  public List<Menu> list() {
    return menuDAO.queryMulti("SELECT * FROM menu", Menu.class);
  }

  // 根据菜品 ID 获取菜品详情 (获取单价)
  public Menu getMenuById(int id) {
    return menuDAO.querySingle("SELECT * FROM menu WHERE id = ?", Menu.class, id);
  }
}
  • 表现层交互逻辑:用户选择 3 显示所有菜品,调用 menuService.list() 格式化打印菜品编号、名称、类别及单价。

    image-20260831163203446

开台点餐 ​

业务流程与表设计 ​

核心经营业务:输入餐桌号、菜品号与数量后,系统自动生成 UUID 流水号,计算总金额并插入 bill 表,同时联动将该餐桌状态更改为 "就餐中"。

字段名类型约束业务说明
idINTPRIMARY KEY, AUTO_INCREMENT账单流水主键
billIdVARCHAR(64)NOT NULL全局唯一业务订单号 (UUID)
menuIdINTNOT NULL菜品编号 (关联 menu.id)
numsINTNOT NULL点餐份数
moneyDOUBLE(10,2)NOT NULL该项小计 (nums * menu.price)
diningTableIdINTNOT NULL就餐桌号 (关联 diningTable.id)
billDateDATETIMENOT NULL点餐时间戳
stateVARCHAR(50)NOT NULL状态:未结账/现金/微信/支付宝

实体与持久层 ​

注意事项:数据库日期类型映射

  • 使用 MySQL 8 查询 DATETIME 或 TIMESTAMP 字段时,驱动可能返回 java.time.LocalDateTime。

  • Java 实体类中的日期属性类型必须与数据库返回类型兼容。

  • 如果实体类使用 java.util.Date,可能出现:Cannot set date: incompatible types

  • 推荐将实体类中的日期字段统一改为 LocalDateTime:

    java
    private LocalDateTime date;
    
    public LocalDateTime getDate() {
        return date;
    }
    
    public void setDate(LocalDateTime date) {
        this.date = date;
    }
java
// 实体类 Bill.java
package com.mhl.domain;
import java.time.LocalDateTime;

public class Bill {
  private Integer id;
  private String billId;
  private Integer menuId;
  private Integer nums;
  private Double money;
  private Integer diningTableId;
  private LocalDateTime billDate;
  private String state;

  public Bill() {}
  // Getters and Setters...
  public Integer getId() { return id; }
  public void setId(Integer id) { this.id = id; }
  public String getBillId() { return billId; }
  public void setBillId(String billId) { this.billId = billId; }
  public Integer getMenuId() { return menuId; }
  public void setMenuId(Integer menuId) { this.menuId = menuId; }
  public Integer getNums() { return nums; }
  public void setNums(Integer nums) { this.nums = nums; }
  public Double getMoney() { return money; }
  public void setMoney(Double money) { this.money = money; }
  public Integer getDiningTableId() { return diningTableId; }
  public void setDiningTableId(Integer diningTableId) { this.diningTableId = diningTableId; }
  public LocalDateTime getBillDate() { return billDate; }
  public void setBillDate(LocalDateTime billDate) { this.billDate = billDate; }
  public String getState() { return state; }
  public void setState(String state) { this.state = state; }
}
java
// 数据访问 BillDAO.java
package com.mhl.dao;
import com.mhl.domain.Bill;

public class BillDAO extends BasicDAO<Bill> {}

点餐业务逻辑与交互 ​

注意事项:UUID 写入数据库

  • UUID.randomUUID() 返回的是 UUID 对象,不是字符串。

  • 数据库中的 bill_id 通常为 VARCHAR 类型,不能直接接收 UUID 对象。

  • 写入数据库前必须转换为字符串:

    java
    String billId = UUID.randomUUID().toString();
    // 或者
    bd.update(sql, billId.toString(), ...);
  • bill_id 字段长度应至少为 VARCHAR(36)。

  • 避免使用不指定字段名的 INSERT INTO bill VALUES (...),建议明确列出字段名,防止表结构顺序变化导致插入错误。

java
// 业务层 BillService.java (点餐部分)
package com.mhl.service;

import com.mhl.dao.BillDAO;
import java.util.UUID;

public class BillService {
  private BillDAO billDAO = new BillDAO();
  private MenuService menuService = new MenuService();
  private DiningTableService diningTableService = new DiningTableService();

  public boolean orderMenu(int menuId, int nums, int diningTableId) {
    // 1. 生成全局唯一订单号
    String billId = UUID.randomUUID().toString();
    // 2. 计算金额小计
    double money = menuService.getMenuById(menuId).getPrice() * nums;

    // 3. 写入账单表
    String insertSql = "INSERT INTO bill VALUES(NULL, ?, ?, ?, ?, ?, NOW(), '未结账')";
    int affected = billDAO.update(insertSql, billId, menuId, nums, money, diningTableId);
    if (affected <= 0) return false;

    // 4. 联动更新餐桌状态为 '就餐中'
    return diningTableService.updateDiningTableState(diningTableId, "就餐中");
  }
}
  • 表现层交互逻辑:

    1. 用户选择 4 点餐服务;
    2. 校验餐桌号存在性;
    3. 校验菜品编号存在性;
    4. 输入点餐数量,调用 billService.orderMenu() 完成点餐并更改桌态;
    5. 点餐成功后生成账单。

    image-20260901214631476

账单查询 ​

业务需求与多表 DTO 设计 ​

单表 bill 仅记录了 menuId,无法直观展示菜名与单价。通过多表联查(bill JOIN menu),将结果映射到复合传输对象 MultiTableBean。

java
// 复合传输实体 MultiTableBean.java
package com.mhl.domain;
import java.util.Date;

public class MultiTableBean {
  private Integer id;
  private String billId;
  private Integer menuId;
  private Integer nums;
  private Double money;
  private Integer diningTableId;
  private Date billDate;
  private String state;
  // 扩展字段 (来自 menu 表)
  private String name;
  private Double price;

  public MultiTableBean() {}
  // Getters and Setters...
  public Integer getId() { return id; }
  public void setId(Integer id) { this.id = id; }
  public String getBillId() { return billId; }
  public void setBillId(String billId) { this.billId = billId; }
  public Integer getMenuId() { return menuId; }
  public void setMenuId(Integer menuId) { this.menuId = menuId; }
  public Integer getNums() { return nums; }
  public void setNums(Integer nums) { this.nums = nums; }
  public Double getMoney() { return money; }
  public void setMoney(Double money) { this.money = money; }
  public Integer getDiningTableId() { return diningTableId; }
  public void setDiningTableId(Integer diningTableId) { this.diningTableId = diningTableId; }
  public Date getBillDate() { return billDate; }
  public void setBillDate(Date billDate) { this.billDate = billDate; }
  public String getState() { return state; }
  public void setState(String state) { this.state = state; }
  public String getName() { return name; }
  public void setName(String name) { this.name = name; }
  public Double getPrice() { return price; }
  public void setPrice(Double price) { this.price = price; }
}

多表持久层与业务实现 ​

注意事项:数据库字段与 Java 属性映射

  • 数据库字段通常使用下划线命名,例如:menu_id、dining_table_id。
  • Java 实体类通常使用驼峰命名,例如:menuId、diningTableId。
  • 使用 BeanListHandler 等工具自动封装查询结果时,不能保证自动将下划线字段转换为驼峰属性。
  • 若字段名与实体属性名不一致,实体对应属性可能为 null,即使数据库中实际有数据。

解决方式:在 SQL 查询中为字段显式设置别名,使其与 Java 属性名一致。

java
String sql = "SELECT id, bill_id AS billId, menu_id AS menuId, num, amount, " +
        "dining_table_id AS diningTableId, date, state FROM bill;";

这样可确保:

  • bill_id → billId
  • menu_id → menuId
  • dining_table_id → diningTableId
java
// MultiTableDAO.java
package com.mhl.dao;
import com.mhl.domain.MultiTableBean;

public class MultiTableDAO extends BasicDAO<MultiTableBean> {}
java
// 业务层 BillService.java (查询部分)
public List<MultiTableBean> listMultiTable() {
  String sql = """
    SELECT bill.*, menu.name, menu.price 
      FROM bill 
      LEFT JOIN menu ON bill.menuId = menu.id 
      ORDER BY bill.id DESC
    """;
  return new MultiTableDAO().queryMulti(sql, MultiTableBean.class);
}
  • 表现层交互逻辑:用户选择 5 查看账单,调用 billService.listMultiTable() 并在控制台打印出包含菜品名称、单价、数量、总价、桌号与时间状态的明细清单。

    image-20260831172721172

结账买单 ​

业务流程与规则校验 ​

完成就餐消费结算,形成业务闭环:

  1. 未结校验:检查目标餐桌是否存在状态为 '未结账' 的流水;

  2. 状态核销:将该桌所有未结账单状态更新为指定支付方式(现金 / 微信 / 支付宝);

  3. 释放餐桌:将餐桌状态重置为 '空闲',并清空客户预订姓名与电话。

image-20260831174015162

结账业务实现 ​

java
// 业务层 BillService.java (结账部分)
public boolean hasUnpaidBill(int diningTableId) {
  String sql = "SELECT * FROM bill WHERE diningTableId = ? AND state = '未结账' LIMIT 1";
  return billDAO.querySingle(sql, Bill.class, diningTableId) != null;
}

public boolean payBill(int diningTableId, String payMode) {
  // 1. 更新该桌所有未结账单
  String sql = "UPDATE bill SET state = ? WHERE diningTableId = ? AND state = '未结账'";
  int affected = billDAO.update(sql, payMode, diningTableId);
  if (affected <= 0) return false;

  // 2. 释放餐桌
  return diningTableService.updateDiningTableToFree(diningTableId);
}

事务演进 ​

数据一致性与 ThreadLocal 事务 ​

在“点餐(写账单 + 改桌态)”和“结账(核销账单 + 释放餐桌)”两个核心场景中,涉及多条 SQL 操作。若使用不同数据库连接,中途发生异常将造成脏数据(例如:账单写入成功但餐桌状态仍为空闲)。

  • 进阶改造方案:
    1. 在 JDBCUtilsByDruid 中引入 ThreadLocal<Connection>,使同一个线程在一次业务调用中共享同一个数据库连接;
    2. 在 Service 层调用 connection.setAutoCommit(false) 开启显式事务;
    3. 业务全部执行完毕后 commit();若捕获异常则统一 rollback(),保证操作的原子性(Atomicity)。

架构重构路线图 ​

演进阶段技术栈核心改造与收益
阶段 1:Web 化改造Servlet + JSP + Tomcat摆脱控制台黑白窗口,实现基于浏览器的可视化点餐与餐桌平面图监控
阶段 2:框架整合 (SSM)Spring + Spring MVC + MyBatis引入 IoC 容器解耦组件,利用 AOP 实现声明式 @Transactional 事务,用 MyBatis XML 动态 SQL 替代手写 DBUtils
阶段 3:前后端分离云原生Spring Boot 3 + Vue 3 + Redis前后端分离架构,Redis 缓存热门菜品数据,Spring Security + JWT 支撑基于 RBAC 的动态员工权限控制

附录:数据脚本 ​

MySQL 初始化脚本 ​

sql
-- 1. 创建数据库
CREATE DATABASE IF NOT EXISTS `mhl` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE `mhl`;

-- 2. 员工表
DROP TABLE IF EXISTS `employee`;
CREATE TABLE `employee` (
  `id` INT PRIMARY KEY AUTO_INCREMENT,
  `empId` VARCHAR(50) NOT NULL UNIQUE,
  `pwd` CHAR(32) NOT NULL,
  `name` VARCHAR(50) NOT NULL,
  `job` VARCHAR(50) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 3. 餐桌表
DROP TABLE IF EXISTS `diningTable`;
CREATE TABLE `diningTable` (
  `id` INT PRIMARY KEY AUTO_INCREMENT,
  `state` VARCHAR(20) NOT NULL DEFAULT '空闲',
  `orderName` VARCHAR(50) NOT NULL DEFAULT '',
  `orderTel` VARCHAR(20) NOT NULL DEFAULT ''
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 4. 菜谱表
DROP TABLE IF EXISTS `menu`;
CREATE TABLE `menu` (
  `id` INT PRIMARY KEY AUTO_INCREMENT,
  `name` VARCHAR(50) NOT NULL,
  `type` VARCHAR(50) NOT NULL,
  `price` DOUBLE(10,2) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 5. 账单表
DROP TABLE IF EXISTS `bill`;
CREATE TABLE `bill` (
  `id` INT PRIMARY KEY AUTO_INCREMENT,
  `billId` VARCHAR(64) NOT NULL,
  `menuId` INT NOT NULL,
  `nums` INT NOT NULL,
  `money` DOUBLE(10,2) NOT NULL,
  `diningTableId` INT NOT NULL,
  `billDate` DATETIME NOT NULL,
  `state` VARCHAR(50) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 6. 初始化种子数据
-- 员工默认密码:666666 (MD5: f379eaf32cedb6307f66cfce04b829ac), 123456 (MD5: e10adc3949ba59abbe56e057f20f883e)
INSERT INTO `employee` (`empId`, `pwd`, `name`, `job`) VALUES
('6666', MD5('666666'), '韩顺平', '大堂经理'),
('0001', MD5('123456'), '张三', '主厨'),
('0002', MD5('123456'), '李四', '前台接待');

INSERT INTO `diningTable` (`id`, `state`) VALUES
(1, '空闲'), (2, '空闲'), (3, '空闲'), (4, '空闲'), (5, '空闲');

INSERT INTO `menu` (`name`, `type`, `price`) VALUES
('佛跳墙', '特色招牌', 198.00),
('东坡肉', '经典热菜', 58.00),
('西湖醋鱼', '经典热菜', 68.00),
('宫保鸡丁', '川味热菜', 38.00),
('松鼠鳜鱼', '苏帮名菜', 88.00),
('扬州炒饭', '主食精选', 22.00),
('西湖牛肉羹', '经典靓汤', 32.00);