Software Development · โปรเจกต์ทีม
ระบบหน้าร้านสุกี้ชาบูบุฟเฟต์
เปิดโต๊ะ · สั่งอาหาร · ชำระเงิน · หลังบ้านผู้จัดการ

01 · โจทย์
หน้าตาเป็นอย่างไร · สีแบรนด์ แดง #B30004
ใครทำงานอะไร ส่งเมื่อไร · 50 งาน 8 คน
เก็บข้อมูลอะไร · 15 ตาราง + กฎร้าน
02 · ภาพรวม






03 · สถาปัตยกรรม
MemberPanel · PosPanel · LoginFrame — เรียก service เท่านั้น ห้ามแตะ SQLMemberService.add() ตรวจเบอร์ซ้ำ · PosService.checkout() คิดเงินMemberDao รู้ชื่อคอลัมน์ของตาราง Member คนเดียวDatabaseHelper ถือ Connection เดียวของทั้งแอป · Session · DateUtil"กล่องข้อมูล" 1 แถว
เช่น Member, Bill
วิ่งผ่านทุกชั้น
จากฐานข้อมูลถึงหน้าจอ
04 · ฐานข้อมูล
User_Account แทนการแก้ตารางเดิม · ชีทมีงานตรวจนับสต็อก จึงเพิ่ม Check_Stock คู่กับรายละเอียดM001, PD001, B0001 ไม่ใช่เลข AUTOINCREMENT แบบในแลปPRAGMA foreign_keys = ON ทุกครั้งที่เชื่อมต่อ ไม่งั้นลบสมาชิกที่มีบิลอ้างถึงได้05 · helper
newnextId() สร้างรหัสถัดไป เช่น M005 → M006 และ closeQuietly() ให้ DAO ใช้ปิด rs/stmthelper/DatabaseHelper.javapublic static synchronized Connection getConnect() { if (conn == null) { try { conn = DriverManager.getConnection(URL); Statement pragma = null; try { pragma = conn.createStatement(); pragma.execute("PRAGMA foreign_keys = ON"); } finally { closeQuietly(null, pragma); } createTablesIfEmpty(); // ไฟล์ใหม่ = สร้างตาราง + mock data } catch (SQLException ex) { LOG.log(Level.SEVERE, "เชื่อมต่อไม่สำเร็จ", ex); } } return conn; }
06 · model
fromRS สร้างก่อนแล้วค่อยเติมอ่าน 1 แถวที่ ResultSet ชี้อยู่ แล้วเติมทีละคอลัมน์ ชื่อคอลัมน์ต้องตรงกับตารางทุกตัว
20 model ในโปรเจกต์ทำแบบเดียวกันหมด
model/Member.javapublic class Member { private String memberId; // ← Member_ID (PK เช่น M001) private String memberName; // ← Member_Name private String memberPhoneNo; // ← UNIQUE 10 หลัก private String memberAddress; // ← NULL ได้ private int pointBalance; // constructor 3 แบบ + getter/setter (Ctrl+I) public static Member fromRS(ResultSet rs) throws SQLException { Member m = new Member(); m.setMemberId(rs.getString("Member_ID")); m.setMemberName(rs.getString("Member_Name")); m.setMemberPhoneNo(rs.getString("Member_PhoneNo")); m.setMemberAddress(rs.getString("Member_Address")); m.setPointBalance(rs.getInt("Point_Balance")); return m; } }
07 · dao
dao/Dao.javapublic interface Dao<T> { T get(String id); // ไม่พบคืน null List<T> getAll(); List<T> getAll(String where, String order); T save(T obj); // สำเร็จคืน obj ที่มีรหัสแล้ว T update(T obj); // ไม่พบแถวคืน null int delete(T obj); // จำนวนแถวที่ลบ, -1 = ผิดพลาด }
| Lab 8 | โปรเจกต์นี้ | |
|---|---|---|
| เมธอด | 6 ตัว | 6 ตัว ชื่อเดียวกัน |
| ชนิดรหัส | int id | String id (M001) |
| รหัสใหม่ | getInsertedId() | nextId() |
| ผิดพลาด | ไม่ throw ออก · log แล้วคืน null / -1 | |
ตารางที่ PK มี 2 คอลัมน์ เช่น Bill_Product ไม่ implements Dao เพราะ get รับรหัสตัวเดียว
08 · dao
dao/MemberDao.java@Override public Member get(String id) { String sql = "SELECT * FROM Member WHERE Member_ID = ?"; Connection conn = DatabaseHelper.getConnect(); PreparedStatement stmt = null; ResultSet rs = null; try { stmt = conn.prepareStatement(sql); stmt.setString(1, id); rs = stmt.executeQuery(); return rs.next() ? Member.fromRS(rs) : null; } catch (SQLException ex) { LOG.log(Level.WARNING, "get(" + id + ") ล้มเหลว", ex); return null; } finally { DatabaseHelper.closeQuietly(rs, stmt); } }
09 · service
service/MemberService.java (ย่อ)/** สมัครสมาชิกด่วนหน้าร้าน */ public Member add(Member m) { validate(m); // ชื่อห้ามว่าง เบอร์ 10 หลัก if (memberDao.getByPhoneNo(m.getMemberPhoneNo()) != null) { throw new IllegalArgumentException( "เบอร์ " + m.getMemberPhoneNo() + " เป็นสมาชิกอยู่แล้ว"); } return memberDao.save(m); } /** ค้นในหน่วยความจำ ไม่ต่อข้อความผู้ใช้เข้า SQL */ public List<Member> search(String keyword) { return memberDao.getAll().stream() .filter(x -> x.getMemberName().contains(keyword) || x.getMemberPhoneNo().contains(keyword)) .collect(Collectors.toList()); }
getAll() · get(id) · search() · add() · edit() · remove()
คนทำหน้าจอเดาชื่อได้โดยไม่ต้องถามเจ้าของโมดูล
หน้าจอ catch แล้วโชว์ ex.getMessage() ใน JOptionPane ทุกหน้าจึงได้กฎเดียวกัน
remove() ลบแถวที่มีคนอ้างถึงไม่ได้ (ติด foreign key) จะคืน false ให้หน้าจอบอกเหตุผล
10 · ทดสอบ
เจอ SQL ผิดตอนนี้ แก้ง่ายกว่าตอนต่อหน้าจอแล้ว
11 · หน้าจอ

ui → New → JPanel Form (หน้าใน MainFrame), JFrame Form (Login, Main), JDialog Form (popup)txt txa cmb btn tbl scr + ชื่อ field.form และเขียน initComponents() ในบล็อกสีเทาให้เอง ห้ามแก้ด้วยมือinitComponents() ผ่าน Theme เท่านั้น12 · หน้าจอ
btnSaveActionPerformed(evt)DefaultTableModel ใหม่ทุกครั้งจากข้อมูลสด แล้ว setModelหน้า CRUD 7 หน้า (สมาชิก สินค้า วัตถุดิบ ผู้จัดจำหน่าย พนักงาน เงินเดือน ค่าใช้จ่าย) ใช้โครงนี้
ui/MemberPanel.java (ส่วนที่เขียนเอง · ย่อ)public MemberPanel() { initComponents(); tblMember.getSelectionModel() .addListSelectionListener(this::tblMemberValueChanged); loadTable(memberService.getAll()); } private void loadTable(List<Member> list) { DefaultTableModel model = Form.readOnlyModel( "รหัส", "ชื่อ-นามสกุล", "เบอร์โทร", "แต้ม"); for (Member m : list) { model.addRow(new Object[]{m.getMemberId(), m.getMemberName(), ...}); } tblMember.setModel(model); } private void btnSaveActionPerformed(ActionEvent evt) { try { if (id == null) memberService.add(m); else memberService.edit(m); loadTable(memberService.getAll()); } catch (IllegalArgumentException ex) { JOptionPane.showMessageDialog(this, ex.getMessage()); } }
13 · ล็อกอิน

Session จำว่าใครล็อกอินอยู่ ทุกหน้าเรียก Session.isManager() ได้โดยไม่ต้องส่งต่อทาง constructor
service/LoginService.javapublic UserAccount login(String username, String password, String role) { UserAccount user = userAccountDao.getByUsername(username); if (user == null || !user.getPassword().equals(password) || !user.getRole().equals(role)) { return null; } Employee employee = employeeDao.get(user.getEmpId()); Session.login(user, employee); return user; } ui/LoginFrame.java (ย่อ)private void btnLoginActionPerformed(ActionEvent evt) { String role = tglManager.isSelected() ? UserAccount.ROLE_MANAGER : UserAccount.ROLE_EMPLOYEE; UserAccount user = loginService.login(...); if (user == null) { // แจ้งเตือนแล้วล้างช่องรหัส return; } new MainFrame().setVisible(true); dispose(); }
14 · เมนูหลัก

LoginFrame กับ MainFrame หน้าอื่นเป็น JPanelpnlContent ตั้ง Layout เป็น Card Layout ในแท็บ Design แล้ว add(page, ชื่อการ์ด)private void showCard(String card, JButton active) { Component page = pages.get(card); if (page instanceof Refreshable) { ((Refreshable) page).refresh(); // โหลดข้อมูลสด } cards.show(pnlContent, card); }
15 · ขายหน้าร้าน
OpenTableDialog เลือกโต๊ะ + จำนวนคนPosService.openTable() เช็กโต๊ะว่าง และคนไม่เกินที่นั่งBillDao.save() สร้างบิล B0007DiningTableDao.updateStatus()ว่าง → กำลังใช้งาน
PosService.addProduct() เมนูเดิมบวกจำนวนในแถวเดิมOrderLinePanel ปุ่ม − / + / ลบโต๊ะที่มีบิลค้าง เปิดใหม่ = สั่งต่อในบิลเดิม
PaymentDialog ค้นสมาชิกด้วยเบอร์ เลือกโปรPosService.checkout() ใน transaction เดียวBillDetailDialog พิมพ์ใบเสร็จกำลังใช้งาน → ว่าง
16 · ชำระเงิน
ในแลป ถ้าปิดบิลสำเร็จแต่คืนโต๊ะพัง โต๊ะจะค้าง ทางแก้คือรวมเป็น ธุรกรรมเดียว
require() แล้ว throw เอง ไม่งั้น rollback ไม่ทำงานservice/PosService.java (ย่อ)public Bill checkout(...) throws SQLException { Connection conn = DatabaseHelper.getConnect(); conn.setAutoCommit(false); try { double discount = promotionService.calcDiscount(...); double net = round2(total - discount); if (received < net) { throw new IllegalArgumentException("รับเงินไม่พอ ..."); } require(billDao.update(bill) != null, "บันทึกบิลไม่สำเร็จ"); stockItemService.deductByBom(orderId); memberService.addPoints(memberId, net); diningTableDao.updateStatus(tableId, DiningTable.STATUS_VACANT); conn.commit(); return bill; } catch (RuntimeException | SQLException ex) { conn.rollback(); throw ex; } finally { conn.setAutoCommit(true); } }
17 · กฎร้าน
ลดค่าบุฟเฟต์ 1 ท่าน (ใบที่ถูกที่สุด)
ต้องค้นหาสมาชิกก่อน ไม่งั้นกดยืนยันไม่ได้
ลดราคาน้ำซุปพิเศษ 1 หม้อ
PromotionServicedao/StockItemDao.java · ตัดสต็อกด้วย SQL คำสั่งเดียวUPDATE Stock_Item SET Current_Stock = Current_Stock - ( SELECT SUM(bp.Unit * bom.Quantity) FROM Bill_Product bp JOIN BOM bom ON bom.Product_ID = bp.Product_ID WHERE bp.Order_ID = ? AND bom.Stock_Item_ID = Stock_Item.Stock_Item_ID) WHERE Stock_Item_ID IN (...วัตถุดิบที่บิลนี้ใช้...)
18 · Dashboard

dao/ReportDao.java · ยอดขายรายเดือนSELECT strftime('%m', Payment_Date) AS label, SUM(Total_Price - Discount_Amount) AS value FROM Bill WHERE Payment_Date IS NOT NULL AND strftime('%Y', Payment_Date) = ? GROUP BY label ORDER BY label
19 · สรุปที่มา
| ส่วนของโปรเจกต์ | มาจาก | ไฟล์ในโปรเจกต์ |
|---|---|---|
| แบ่ง 4 ชั้น | Lab 8 · หัวข้อ 0.1 | ui/ service/ dao/ helper/ model/ |
| ฐานข้อมูล + foreign key | Lab 8 · ช่วง 2 | sql/bonus_suki_sqlite.sql, bonus_suki_mock_data.sql |
| Connection กลาง | Lab 8 · ช่วง 3 | DatabaseHelper |
| model + fromRS | Lab 8 · ช่วง 5 | 20 คลาสใน model/ |
| Dao<T> + try-catch-finally | Lab 8 · ช่วง 6 | 20 คลาสใน dao/ |
| service + ทดสอบ | Lab 8 · ช่วง 7 · POS Flow Step 5 | 15 คลาสใน service/, TestMemberDao, TestPosService |
| หน้าจอลากวาง + CRUD | Lab 11 · Step 3–7 | 18 หน้าใน ui/ (มี .form ทุกหน้า) |
| Login + สิทธิ์ | Lab 11 · Step 3, 4 | LoginFrame, LoginService, Session |
| เปิดโต๊ะ / สั่ง / เช็คบิล | POS Flow · Step 1–11 | PosPanel, OpenTableDialog, PaymentDialog, PosService |
| transaction ตอนจ่าย | POS Flow · แบบฝึกหัดข้อ 3 | PosService.checkout() |
20 · ทีม
bonus_suki.db, DatabaseHelper, Dao, Theme, ThaiFont · StockItemPanel, CheckStockPanel
LoginService, Session, LoginFrame, MainFrame, HomePanel · Expense ครบชุด
PosService, PromotionService, DiningTable · ProductItemPanel, OrderLinePanel, OpenTableDialog, PaymentDialog
Bill, BillProduct, BillService · BillPanel, BillDetailDialog · ทดสอบทุกหน้า
Product, StockItem, CheckStock ครบชุด DAO/Service · ProductPanel
Employee, UserAccount, Vendor ครบชุด · EmployeePanel, VendorPanel, PosPanel
Member (โมดูลแม่แบบ), Payroll ครบชุด · MemberPanel, PayrollPanel
WorkTime ครบชุด, ReportDao, ReportService · WorkTimePanel, DashboardPanel · Scenarios, ER
21 · สรุป
Q&A
ลองใช้งานจริง
รัน BonusSuki.java ใน NetBeans (F6)
manager / 1234 · กดสถานะ "ผู้จัดการ"