畢設(shè):三端分離架構(gòu)與數(shù)據(jù)庫設(shè)計實戰(zhàn))
簡介這份資源是面向高校計算機(jī)相關(guān)專業(yè)學(xué)生的奶茶店點餐微信小程序畢業(yè)設(shè)計完整方案包含小程序前端、后臺管理系統(tǒng)與數(shù)據(jù)庫三部分適合作為畢設(shè)、期末大作業(yè)或課程設(shè)計選題也適合想入門微信小程序開發(fā)的新手參考。壓縮包共481個文件約5.64MB其中101個Java文件構(gòu)成后臺服務(wù)端邏輯57個JS與50個Vue文件支撐前端頁面與交互另有106個PNG、104個JPG等圖片資源用于界面展示并附帶SQL建庫腳本、YML配置、MD說明文檔及字體文件代碼注釋齊全配有使用文檔部署后即可運行。目前已有407人學(xué)習(xí)關(guān)注。項目功能完善、界面美觀、操作簡單涵蓋點餐、訂單管理、后臺維護(hù)等模塊讀者可據(jù)此快速理解小程序與后臺的聯(lián)調(diào)方式掌握從數(shù)據(jù)庫設(shè)計到接口實現(xiàn)的完整流程也可直接用于答辯展示或二次開發(fā)。1. 奶茶店點餐小程序畢設(shè)從選題到能跑起來的那條最短路徑每年畢業(yè)季后臺被問得最多的一類問題就是基于微信小程序的奶茶店點餐系統(tǒng)到底能不能做、要花多久、答辯會不會被問穿。先說結(jié)論——這個選題是典型的「看起來簡單、做起來有細(xì)節(jié)、答辯有話說」的高分畢設(shè)方向。它同時踩中了微信小程序、點餐業(yè)務(wù)、后臺系統(tǒng)、數(shù)據(jù)庫設(shè)計四個可展示的技術(shù)面老師能問的點多你能講的深度也夠。但絕大多數(shù)人翻車不是因為不會寫代碼而是因為一開始就沒想清楚小程序端、后臺管理端、數(shù)據(jù)庫這三塊到底怎么分工數(shù)據(jù)怎么流轉(zhuǎn)哪些功能是必須做的哪些是加分項。這篇筆記就按我實際帶過幾屆畢設(shè)的經(jīng)驗把這條路徑從頭到尾拆一遍讓你看完能直接動手而不是繼續(xù)在選題階段反復(fù)橫跳。2. 先想清楚三端分工小程序、后臺、數(shù)據(jù)庫各管什么2.1 為什么這個選題必須做成「三端分離」很多同學(xué)一上來就想用云開發(fā)一把梭覺得不用寫后臺多省事。但畢設(shè)答辯有個潛規(guī)則老師希望看到你具備完整的工程能力而不是只會調(diào)云函數(shù)。三端分離的結(jié)構(gòu)——微信小程序負(fù)責(zé)用戶交互、后臺系統(tǒng)負(fù)責(zé)商家管理、數(shù)據(jù)庫負(fù)責(zé)數(shù)據(jù)持久化——恰好能覆蓋前端、后端、數(shù)據(jù)庫三個維度的考核點。而且這個結(jié)構(gòu)在真實奶茶店場景里也是成立的顧客掃碼點單、店員在后臺接單改庫存、老板看營業(yè)數(shù)據(jù)三條線本來就是分開的。從技術(shù)選型上說小程序端用原生微信小程序框架就夠了沒必要上 uniapp 增加復(fù)雜度除非你導(dǎo)師明確要求跨平臺。后臺系統(tǒng)我一般推薦用 Vue Element UI 或者直接 SpringBoot 帶一個簡單的管理頁面看你的技術(shù)棧。數(shù)據(jù)庫用 MySQL 最穩(wěn)資料多、老師熟、出問題好查。這里有個血淚經(jīng)驗不要為了顯得高級去用 MongoDB 或者 Redis 做主存儲畢設(shè)場景數(shù)據(jù)量小、關(guān)系明確MySQL 的關(guān)系模型反而更好講清楚。2.2 三端各自的最小功能集小程序端必須有的商品列表按分類展示奶茶、商品詳情規(guī)格選擇杯型、糖度、溫度、加料、購物車、下單結(jié)算、訂單列表、訂單詳情、個人中心。這些是點餐系統(tǒng)的骨架缺一個老師都會問「為什么沒有」。加分項可以加微信支付沙箱環(huán)境即可、優(yōu)惠券、積分、評價。后臺系統(tǒng)必須有的商品管理增刪改查、上下架、分類管理、訂單管理接單、完成、取消、用戶管理、數(shù)據(jù)統(tǒng)計今日訂單數(shù)、銷售額。這里注意訂單管理一定要有狀態(tài)流轉(zhuǎn)不能只做一個列表否則答辯時老師問「訂單從下單到完成經(jīng)歷了什么」你就卡住了。數(shù)據(jù)庫至少要設(shè)計這幾張表用戶表、商品表、商品分類表、規(guī)格表杯型/糖度/溫度、購物車表、訂單表、訂單明細(xì)表、管理員表。表之間的關(guān)系要能畫出來一對多、多對多各在哪里這是答辯必問。2.3 一個容易忽略的坑規(guī)格和庫存的耦合奶茶點餐和普通電商最大的區(qū)別在于規(guī)格組合。一杯奶茶可能有杯型中杯/大杯、糖度無糖/三分/五分/七分/全糖、溫度常溫/少冰/多冰/熱、加料珍珠/椰果/布丁四個維度。如果你把每個組合都當(dāng)成一個獨立 SKU 存進(jìn)商品表數(shù)據(jù)量會爆炸如果只存基礎(chǔ)商品下單時又沒法準(zhǔn)確扣庫存。常見做法是商品表存基礎(chǔ)信息規(guī)格表存可選項訂單明細(xì)表里用 JSON 字段記錄用戶選擇的具體規(guī)格組合。庫存扣減只針對基礎(chǔ)商品和加料杯型糖度溫度不影響庫存。這個設(shè)計思路答辯時講出來老師會覺得你真的想過業(yè)務(wù)。3. 數(shù)據(jù)庫設(shè)計八張表撐起整個點餐流程3.1 核心表結(jié)構(gòu)設(shè)計先看整體 ER 關(guān)系用戶下單產(chǎn)生訂單訂單包含多個訂單明細(xì)每個明細(xì)對應(yīng)一個商品和一組規(guī)格選擇。商品屬于某個分類商品有多個規(guī)格選項。管理員獨立于普通用戶。下面是我一般會用的建表語句以 MySQL 為例-- 用戶表 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) NOT NULL COMMENT 微信openid, nickname VARCHAR(64) DEFAULT , avatar VARCHAR(255) DEFAULT , phone VARCHAR(20) DEFAULT , create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用戶表; -- 商品分類表 CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL, sort INT DEFAULT 0 COMMENT 排序權(quán)重, status TINYINT DEFAULT 1 COMMENT 1啟用 0禁用 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品分類; -- 商品表 CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL, name VARCHAR(64) NOT NULL, price DECIMAL(10,2) NOT NULL, image VARCHAR(255) DEFAULT , description VARCHAR(255) DEFAULT , stock INT DEFAULT 0, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表; -- 規(guī)格選項表杯型/糖度/溫度/加料統(tǒng)一存這里 CREATE TABLE spec_option ( id INT PRIMARY KEY AUTO_INCREMENT, type VARCHAR(16) NOT NULL COMMENT cup/sugar/temp/topping, name VARCHAR(32) NOT NULL, extra_price DECIMAL(10,2) DEFAULT 0.00, sort INT DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT規(guī)格選項; -- 訂單表 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待接單 1制作中 2已完成 3已取消, remark VARCHAR(255) DEFAULT , create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT訂單表; -- 訂單明細(xì)表 CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, product_name VARCHAR(64) NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, spec_json JSON COMMENT 規(guī)格組合, KEY idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT訂單明細(xì);這幾張表的設(shè)計邏輯用戶表用 openid 做唯一鍵因為微信小程序登錄拿到的就是 openid商品表用 category_id 關(guān)聯(lián)分類status 控制上下架規(guī)格選項表用 type 字段區(qū)分杯型、糖度、溫度、加料extra_price 處理加料加價訂單表用 order_no 做業(yè)務(wù)單號status 做狀態(tài)流轉(zhuǎn)訂單明細(xì)表用 spec_json 存規(guī)格組合避免多表關(guān)聯(lián)查詢。參數(shù)說明price 用 DECIMAL(10,2) 而不是 FLOAT避免精度問題spec_json 用 MySQL 5.7 的 JSON 類型如果學(xué)校機(jī)房版本低就改成 TEXT 存 JSON 字符串所有表用 utf8mb4 字符集支持 emoji。3.2 購物車表要不要單獨建購物車有兩種做法存本地緩存wx.setStorageSync或者存數(shù)據(jù)庫。我一般建議畢設(shè)場景存數(shù)據(jù)庫因為老師會問「換手機(jī)購物車還在嗎」存本地就答不上來。購物車表結(jié)構(gòu)簡單user_id、product_id、quantity、spec_json、create_time。查詢時關(guān)聯(lián)商品表拿最新價格和圖片。CREATE TABLE cart ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, product_id INT NOT NULL, quantity INT DEFAULT 1, spec_json JSON, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT購物車;注意購物車不需要存價格因為價格可能變動結(jié)算時以商品表當(dāng)前價格為準(zhǔn)。這個細(xì)節(jié)答辯時能講出來是加分項。4. 小程序端實現(xiàn)從商品列表到下單的完整鏈路4.1 請求封裝與全局配置小程序端第一件事不是寫頁面而是封裝請求。原生 wx.request 回調(diào)寫法太亂我一般會封裝成 Promise// utils/request.js const BASE_URL http://localhost:8080/api; function request(options) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { content-type: application/json, token: wx.getStorageSync(token) || }, success(res) { if (res.statusCode 200 res.data.code 0) { resolve(res.data.data); } else if (res.data.code 401) { // token過期重新登錄 wx.removeStorageSync(token); wx.showToast({ title: 請重新登錄, icon: none }); reject(res.data); } else { wx.showToast({ title: res.data.msg || 請求失敗, icon: none }); reject(res.data); } }, fail(err) { wx.showToast({ title: 網(wǎng)絡(luò)異常, icon: none }); reject(err); } }); }); } module.exports { request };邏輯說明統(tǒng)一拼接 BASE_URL統(tǒng)一攜帶 token統(tǒng)一處理業(yè)務(wù)碼。code 0 表示成功401 表示登錄態(tài)失效。參數(shù)說明BASE_URL 在開發(fā)階段指向本地后端上線要改成備案域名但畢設(shè)答辯用本地或局域網(wǎng) IP 即可。4.2 商品列表與規(guī)格選擇商品列表頁按分類展示左側(cè)分類導(dǎo)航右側(cè)商品列表。核心是點擊商品后彈出規(guī)格選擇彈窗用戶選完加入購物車。// pages/menu/menu.js Page({ data: { categories: [], products: [], currentCategory: 0, showSpec: false, currentProduct: null, selectedSpec: { cup: 0, sugar: 0, temp: 0, toppings: [] } }, onLoad() { this.loadCategories(); }, async loadCategories() { const categories await request({ url: /category/list }); this.setData({ categories }); if (categories.length 0) { this.loadProducts(categories[0].id); } }, async loadProducts(categoryId) { const products await request({ url: /product/list, data: { categoryId } }); this.setData({ products, currentCategory: categoryId }); }, openSpec(e) { const product e.currentTarget.dataset.product; this.setData({ showSpec: true, currentProduct: product, selectedSpec: { cup: 0, sugar: 0, temp: 0, toppings: [] } }); }, async addToCart() { const { currentProduct, selectedSpec } this.data; await request({ url: /cart/add, method: POST, data: { productId: currentProduct.id, quantity: 1, spec: selectedSpec } }); wx.showToast({ title: 已加入購物車 }); this.setData({ showSpec: false }); } });邏輯說明onLoad 先加載分類再加載第一個分類下的商品。openSpec 打開規(guī)格彈窗并重置選擇。addToCart 把商品 ID 和規(guī)格組合發(fā)給后端。參數(shù)說明selectedSpec 里的 cup/sugar/temp 存選項 IDtoppings 存數(shù)組因為可以多選。后端收到后根據(jù) ID 查規(guī)格表算加價。4.3 下單與訂單狀態(tài)流轉(zhuǎn)下單接口要做三件事校驗庫存、生成訂單、扣減庫存。這三步必須在一個事務(wù)里否則會出現(xiàn)超賣。// 后端偽代碼SpringBoot Transactional public OrderVO createOrder(Integer userId, OrderDTO dto) { // 1. 校驗庫存 for (OrderItemDTO item : dto.getItems()) { Product product productMapper.selectById(item.getProductId()); if (product.getStock() item.getQuantity()) { throw new BusinessException(庫存不足 product.getName()); } } // 2. 生成訂單 Orders order new Orders(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setStatus(0); order.setTotalPrice(calcTotal(dto)); orderMapper.insert(order); // 3. 扣減庫存 寫明細(xì) for (OrderItemDTO item : dto.getItems()) { productMapper.decrStock(item.getProductId(), item.getQuantity()); OrderItem oi new OrderItem(); oi.setOrderId(order.getId()); oi.setProductId(item.getProductId()); oi.setQuantity(item.getQuantity()); oi.setSpecJson(JSON.toJSONString(item.getSpec())); orderItemMapper.insert(oi); } return buildOrderVO(order); }邏輯說明Transactional 保證三步要么全成功要么全回滾。先校驗再扣減避免扣了庫存但訂單沒生成。參數(shù)說明generateOrderNo 一般用時間戳加隨機(jī)數(shù)decrStock 用 UPDATE product SET stock stock - ? WHERE id ? AND stock ? 這種寫法防止并發(fā)超賣。訂單狀態(tài)流轉(zhuǎn)0 待接單 → 1 制作中 → 2 已完成或者 0 → 3 已取消。后臺接單改狀態(tài) 1制作完成改狀態(tài) 2。小程序端訂單列表按狀態(tài)篩選展示。5. 后臺系統(tǒng)與聯(lián)調(diào)商家端怎么接單改庫存5.1 后臺最小實現(xiàn)方案后臺系統(tǒng)如果時間緊可以用現(xiàn)成的 admin 模板套。我一般推薦兩種Vue Element UI 前后端分離或者 SpringBoot Thymeleaf 服務(wù)端渲染。前者適合你前端還行的情況后者適合只想寫 Java 的情況。核心頁面就四個登錄頁、商品管理、訂單管理、數(shù)據(jù)統(tǒng)計。商品管理要支持列表分頁、新增、編輯、上下架、刪除。訂單管理要支持列表按狀態(tài)篩選、接單、完成、取消。數(shù)據(jù)統(tǒng)計展示今日訂單數(shù)和銷售額用一條 SQL 就能查SELECT COUNT(*) AS order_count, SUM(total_price) AS total_sales FROM orders WHERE DATE(create_time) CURDATE() AND status 2;5.2 前后端聯(lián)調(diào)的三個關(guān)鍵點第一跨域問題。小程序開發(fā)工具里可以在「詳情-本地設(shè)置」勾選「不校驗合法域名」但后臺系統(tǒng)如果前后端分離后端要加 CORS 配置。SpringBoot 加一個配置類即可。第二token 傳遞。小程序登錄后后端返回 token后續(xù)請求放在 header 里。后臺系統(tǒng)用 session 或 JWT 都行畢設(shè)場景 session 更簡單。第三時間格式。數(shù)據(jù)庫存 DATETIME后端返回給前端要統(tǒng)一格式化成 yyyy-MM-dd HH:mm:ss否則小程序端顯示會出問題。5.3 微信登錄的完整流程小程序端 wx.login 拿到 code傳給后端后端調(diào)微信接口換 openid 和 session_key然后生成 token 返回。注意 code 只能用一次且五分鐘過期。// 小程序端登錄 wx.login({ success: async (res) { const data await request({ url: /auth/login, method: POST, data: { code: res.code } }); wx.setStorageSync(token, data.token); wx.setStorageSync(userInfo, data.userInfo); } });后端拿到 code 后請求微信的 code2Session 接口拿到 openid 后查用戶表沒有就注冊有就更新登錄時間最后生成 token。這個流程答辯必問要能畫出來。6. 避坑與排查畢設(shè)答辯前必須過的五道坎6.1 庫存超賣并發(fā)下單時庫存扣成負(fù)數(shù)現(xiàn)象兩個人同時下單最后一杯奶茶庫存變成 -1。原因先查庫存再扣減兩步之間有時間窗口。解決用UPDATE product SET stock stock - ? WHERE id ? AND stock ?根據(jù) affectedRows 判斷是否扣減成功失敗就拋異?;貪L。6.2 訂單號重復(fù)時間戳加隨機(jī)數(shù)也會撞現(xiàn)象偶爾出現(xiàn)訂單號唯一鍵沖突。原因高并發(fā)下時間戳相同、隨機(jī)數(shù)碰撞。解決用時間戳 用戶 ID 后四位 隨機(jī)數(shù)或者直接用數(shù)據(jù)庫自增 ID 拼接前綴。更穩(wěn)的做法是用 Redis 的 INCR 生成序列號但畢設(shè)場景用 UUID 去掉橫線也夠。6.3 規(guī)格 JSON 解析失敗前端傳的格式和后端不一致現(xiàn)象下單時報 JSON 解析異常。原因前端傳的是對象后端用 String 接收或者字段名對不上。解決前后端約定好 spec 的 JSON 結(jié)構(gòu)后端用 RequestBody 接收 DTOspec 字段用 Map 或自定義類接收不要用 String 再手動解析。6.4 小程序請求域名未配置真機(jī)調(diào)試請求失敗現(xiàn)象開發(fā)者工具正常真機(jī)預(yù)覽請求全部失敗。原因微信要求 request 合法域名必須備案且 HTTPS。解決畢設(shè)答辯用開發(fā)者工具演示即可或者用局域網(wǎng) IP 加「不校驗合法域名」選項。如果一定要真機(jī)用內(nèi)網(wǎng)穿透工具臨時映射一個 HTTPS 域名但注意答辯現(xiàn)場網(wǎng)絡(luò)環(huán)境。6.5 數(shù)據(jù)庫中文亂碼emoji 和生僻字顯示問號現(xiàn)象商品名里的 emoji 或者生僻字存進(jìn)去變成問號。原因字符集不是 utf8mb4。解決建庫建表都用 utf8mb4連接字符串加 characterEncodingutf8mb4MySQL 配置文件里也確認(rèn) default-character-setutf8mb4。7. 讓答辯加分的一個技巧把訂單狀態(tài)做成可追溯的時間線大部分畢設(shè)的訂單狀態(tài)就是一個數(shù)字字段改了就改了老師問「這單什么時候接的、什么時候完成的」你答不上來。我一般會加一張訂單狀態(tài)日志表每次狀態(tài)變更都插一條記錄CREATE TABLE order_status_log ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, from_status TINYINT, to_status TINYINT NOT NULL, operator VARCHAR(32) DEFAULT system, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT訂單狀態(tài)日志;然后在訂單詳情頁把這條時間線渲染出來待接單 → 制作中 → 已完成每個節(jié)點帶時間。這個功能代碼量不大但答辯時展示出來老師會覺得你的系統(tǒng)有真實業(yè)務(wù)感而不是簡單的增刪改查。更進(jìn)一步你可以用這條日志做數(shù)據(jù)統(tǒng)計比如算平均制作時長這就是從「能跑」到「有亮點」的差距。我自己的習(xí)慣是每做一個功能先問自己「這個數(shù)據(jù)以后能不能用來講故事」。訂單狀態(tài)日志就是典型例子它本身不復(fù)雜但給了你答辯時展開講的空間。數(shù)據(jù)庫設(shè)計也一樣多一張日志表、多一個統(tǒng)計字段可能就是你從及格到優(yōu)秀的距離。希望幫到你。本文還有配套的精品資源點擊獲取