環境 MySQL Community Server 8.0.25 、Python 3.8、Tkinter
需求分析
本程序模擬一個外賣平台系統,該系統由三端構成即用戶+商家+配送員。
不同的用戶(三類)有不同的ID,姓名可重名。每個用戶可以下多個訂單,每個訂單對應一個配送員,一個訂單內可包含多個同一家店鋪的商品,每個商家可以同時處理來自多個用戶的多個訂單。一個配送員可以同時派送多個訂單,訂單對應的配送員由外賣平台統一分配。
三類用戶均可以以自己的賬號密碼登錄對應的系統,可以注冊新賬號
實現的功能:
- 用戶:登錄/注冊功能,修改用戶信息功能,充值賬戶余額功能,獲取正在營業的店鋪菜單功能,點外賣提交訂單功能,查詢訂單功能。
- 商家:登錄/注冊功能,一鍵上/下班功能 ,修改商家信息功能,營業額提現功能,商品管理模塊包括:查詢商品詳情、新建商品、修改原有商品、刪除商品功能。接單窗口顯示已接單未出餐商品,標記餐品為我已出餐功能,查詢最近已出餐商品功能。
- 配送員:登錄/注冊功能,一鍵上/下班功能 ,修改個人信息功能,工資提現功能,工作窗口顯示派送給自己的所有訂單,修改訂單狀態為已完成(點擊我已送達)功能,查看最近派送完成的訂單功能。

數據庫設計
邏輯結構設計
- 商家:店名,商家號,登錄密碼,地址,電話,營業額,狀態,
- 商品:編號,商品名,價格,商家號,庫存
- 騎手:編號,登錄密碼,姓名,性別,電話,工資,狀態,
- 顧客:ID,登錄密碼,姓名,性別,地址, 電話,余額。
- 訂單:訂單號,騎手號,顧客ID,狀態,備注,配送費,金額,發起時間
- 訂單詳情:訂單號,商品號,數量
關系屬性
- Store(Sno,Spass,Sname,Saddr,Stel,Smoney,Sstate)
- Goods(Gno,Sno,Gname,Gprice,Gstock)
- Customer(Cno,Cpass,Cname,Csex,Caddr,Ctel,Cmoney)
- Deliverer(Dno,Dpass,Dname,Dsex,Dtel,Dmoney,Dstate)
- Order(Ono,Dno,Cno,Ostate,Otip,ODelfee,Omoney,Obtime)
- Purchase(Ono,Gno,Pamount)
E-R圖

數據表
商店表(store)
- 主碼:Sno
- 自定義完整性:
CHECK (Sstate IN (‘工作’,‘休息’)),
CHECK (Smoney >= 0)
商品表(goods)
- 主碼:Gno
- 外碼:FOREIGN KEY (Sno) REFERENCES Store(Sno)
顧客表(Customer)
- 主碼:Cno
- 自定義完整性:
CHECK (Csex IN (‘M’,‘F’)),
CHECK (Cmoney >= 0)
派送員表(deliverer)
- 主碼:Dno
- 自定義完整性:
CHECK (Dsex IN (‘M’,‘F’)),
CHECK (Dstate IN (‘工作’,‘休息’)),
CHECK (Dmoney >= 0)
訂單表(orderr)
- 主碼:Ono
- 外碼:
FOREIGN KEY (Dno) REFERENCES Deliverer(Dno),
FOREIGN KEY (Cno) REFERENCES Customer(Cno) ,
FOREIGN KEY (Sno) REFERENCES Store(Sno) , - 自定義完整性:
CHECK (Omoney >= 0),
CHECK (ODelfee >= 0),
CHECK (Ostate IN(‘正在出餐’,‘正在配送’,‘訂單完成’))
訂單詳情表(purchase)
- 主碼:PRIMARY KEY (Ono, Gno ),
- 外碼:FOREIGN KEY (Ono) REFERENCES Orderr(Ono) ,
FOREIGN KEY (Gno) REFERENCES Goods(Gno) ,
視圖
用戶點單視圖
用戶點單時可以看見所有商家的所有菜品。用戶需要看到的是商店名而不是商店編號,商品名而不是商品編號。
CREATE VIEW view_Cus_buy AS
SELECT Sname '店名',Gname '商品',Gprice '價格'
FROM goods,store
WHERE goods.Sno = store.Sno
AND store.Sstate = '工作'
ORDER BY store.Sno ;
用戶訂單視圖
用戶可以看到訂單的信息。
CREATE VIEW view_Cus_look AS SELECT
orderr.Ono,
store.Sname,
orderr.Omoney,
orderr.Ostate,
orderr.Obtime,
orderr.Cno
FROM orderr,store
WHERE store.Sno = Orderr.Sno
ORDER BY orderr.Obtime DESC ;
密碼視圖
將三類用戶的密碼整合在一起,並根據來源添加類型字段。方便驗證密碼。
CREATE VIEW Password ID,'密碼','類型'AS
SELECT Cno ,Cpass ,'用戶'
FROM Customer
UNION
SELECT Sno,Spass,'商家'
FROM Store
UNION
SELECT Dno,Dpass,'配送員'
FROM Deliverer ;
配送員視圖
騎手無法查看訂單所有信息,需要快速查看地址電話等,故需要單獨設計視圖。
CREATE VIEW view_del_unfinish AS
SELECT store.Sname,customer.Caddress,customer.Ctel,orderr.ODelfee,orderr.Ono
FROM store,customer,orderr
WHERE orderr.Ostate = '正在配送'
AND orderr.Sno=store.Sno
AND orderr.Cno=customer.Cno
ORDER BY orderr.Obtime DESC ;
商家端訂單視圖
商家看到的待出餐的訂單信息。
CREATE VIEW view_store_unfinish AS
SELECT orderr.Ono,customer.Cname,customer.Ctel,orderr.Omoney
FROM store,customer,orderr
WHERE orderr.Ostate = '正在出餐'
AND orderr.Sno=store.Sno
AND orderr.Cno=customer.Cno
ORDER BY orderr.Obtime DESC ;
存儲過程
利用存儲過程實現對金融的操作(扣款/提現/充值)
顧客的充值與扣費函數+配送員獲得工作與提現函數+商家獲得訂單利潤與提現函數。
使用存儲過程的好處:可以使計算過程在服務器完成,減輕本地負擔,對敏感信息的操作與客戶端分離,增強安全性。
DELIMITER $
CREATE PROCEDURE alter_del_money(IN user_id VARCHAR(20),IN amount INT)
BEGIN
update deliverer set Dmoney=amount WHERE Dno=user_id;
END $
DELIMITER ;
觸發器
插入具體商品(Purchase表)時更新訂單(Orderr表)信息
這里在插入一個商品后自動在對應的訂單增加一個數量(對應配送費),修改對應的總金額。
使用觸發器的好處:自動完成修改,防止遺漏,且操作在服務器,與客戶端分離。
BEGIN
DECLARE a INT;
DECLARE b INT;
DECLARE c INT;
SET a = ( SELECT orderr.ODelfee FROM orderr WHERE orderr.Ono = new.Ono );
SET b = ( SELECT orderr.Omoney FROM orderr WHERE orderr.Ono = new.Ono );
SET c = ( SELECT goods.Gprice FROM goods WHERE goods.Gno = new.Gno );
UPDATE orderr
SET orderr.ODelfee = a + 1,
orderr.Omoney = b + c
WHERE
orderr.Ono = new.Ono;
END
UI設計
主界面
三個程序在界面上大體相同,我把他們的主界面分為幾個幾個區域:1.選擇列表區 2.訂單信息區 3.功能區
- 選擇列表區:顯示部分主要是由標題加表格和勾選框以及滑動條組成,操作有全選/取消全選、提交操作、刷新列表。
- 訂單信息區:這個區域是只讀的,只有刷新列表一個功能。
- 功能區:這個區域實時的顯示用戶信息並且有若干功能按鈕,點擊相應的按鈕就進入相應的功能模塊。
主界面是用tkinter.TK()
創建出來的,起到 ‘根界面’ 的作用即其它所有界面都是基於他來做的。創建主界面之后要用mainloop
將其顯示出來。由於實際操作中需要先登錄再進入主界面所以先把主界面用.withdrew()
方法隱藏起來,成功登錄之后再用main_window.deiconify()
將其顯示出來。
登錄界面
打開程序最先跳出的就是登陸界面,在該界面輸入用戶密碼后點擊登錄,如果驗證正確則進入主界面,如果驗證失敗則跳出警告框提示,用戶名密碼信息均來自數據庫。如果賬戶類型和程序不匹配也無法進入程序。
點擊注冊按鈕會跳出注冊界面,輸入信息后點擊確認注冊,則會在對應的用戶類型的數據表中插入一條記錄,注冊成功后就可以用該賬戶登錄系統。
子窗口
點擊一些功能按鈕(如注冊、修改信息、商品管理等)時會跳出一個子窗口在主界面上層以完成相應的模塊的功能。
這里是用tk.Toplevel()
實現的,比如
window_sign_up = tk.Toplevel(loggin_window)
就在loggin_window界面的基礎上創建了window_sign_up子界面。tk.Toplevel()和tk.TK()的功能幾乎完全一樣,不同的是tk.TK()是根界面不建立在其它界面之上。
這里采用模塊化的設計方式,即一個子界面的所有內容都包裝在一個函數(Python允許函數嵌套)或者一個類當中。
我這里大部分都是用函數嵌套來完成的,首先在函數內部定義子功能對應的子函數,然后設計對應子界面上的控件,並在控件的響應函數中調用上述定義的子函數。
消息窗口
messagebox
用於彈出消息窗口,通常用於顯示提示消息,例如登錄成功、登錄失敗
tk.messagebox.showerror(message='密碼錯誤')
tk.messagebox.showinfo(title='welcome',message='登陸成功')
以上是兩類不同的messagebox,一個用來顯示常規信息,一個用來提示錯誤信息。
每個消息窗口都可以用title=‘’來指定信息窗口的標簽,用message=‘’來指定消息窗口的內容。
標簽
label
一般用來顯示靜態的文本或者圖像,比如
tk.Label(main_window, text='用戶名:').place(x=left_index, y=15)
tk.Label(main_window, text='余 額:').place(x=left_index, y=55)
tk.Label(main_window, text='已選:').place(x=40, y=480)
tk.Label(main_window, text='最近購買的訂單:').place(x=440, y=180)
表示在main_window,這個界面上設置標簽,放置位置在(x,y)處,內容是text。
label
還可以用來顯示動態的內容,根據程序的進行不斷改變內容。
total_wallet_show = tk.StringVar() # 用於展示余額
tk.Label(main_window, textvariable=total_wallet_show).place(x=left_index + 40, y=55)
total_wallet_show.set(rest_money)
以上建立了一個標簽,顯示內容為total_wallet_show
,其是tk.StringVar()
類型的變量,然后在程序需要刷新這個內容的時候調用total_wallet_show.set()
將其標簽上的內容刷新。
按鈕
按鈕button
用來實現用戶對某個功能的選擇,button是可以點擊的,點擊相應的button就會跳轉到對應的處理函數。
bt_charge = tk.Button(main_window, text='充值余額', height=1, width=16, command=charge_money)
bt_charge.place(x=left_index - 140, y=50)
bt_user_info = tk.Button(main_window, text='查詢修改個人信息', height=1, width=16, command=alter_info)
bt_user_info.place(x=left_index - 140, y=10)
比如這里設置了bt_charge和bt_user_info兩個按鈕,按鈕放置在main_window的(x,y)位置,按鈕上的顯示信息是test,按鈕大小是height X width,點擊按鈕會跳轉到command指定的函數中去。
列表
本程序中的用戶點單列表、訂單列表、商品管理里面的商品詳情等都是用Treeview
來做的,比如:
columns = ['訂單號', '商家名', '金額', '狀態', '日期']
width_ord = [45, 70, 50, 70, 110]
order_list_table = Treeview( master=main_window, height=10, columns=columns, show='headings')
order_list_table.place(x=440, y=200)
for i in columns:
order_list_table.heading(i, text=i) # 定義表頭
t = 0
for i in columns:
order_list_table.column(i, width=width_ord[t], minwidth=40, anchor=S, ) # 定義列
t += 1
這里定義了一個Tree view表格order_list_table
表格共五列,分別對應columns中的元素。width_ord
的每一項分別是每列的寬度,其父容器是main_window,高度是10行,展示方式是’headings’即展示首行標題
插入待展示的數據的方式:
for row in flu_list: # 格式:店名、商品、價格
i += 1
order_list_table.insert('', 'end', values=row)
這里的flu_list
是一個從數據庫中獲取的二位列表
通過這種方式就把數據庫中的數據展示在表格中了。
勾選框
用戶點單,商家和配送員完成訂單都需要勾選列表中的內容,這里利用 check button
實現,在treeview的每一行前面都設置一個checkbutton,由於treeview和checkbutton都是有編號的,所以可以將每一個勾選框的勾選狀態和列表元素的選擇聯系起來。
ck_button['command'] = lambda item=tv_item: self.select_button(item)
def select_button(self, item):
a = eval("0x" + str(item[1:])) % len(cur_menu)
if a in order_list:
order_list.remove(a)
else:
order_list.add(a)
這里的order_list 集合內存的就是勾選的行對應在菜單內的編號,即勾選了的行的編號都在order_list內。
數據庫相關算法設計
連接數據庫
這里使用的是pymysql連接到本地的mysql數據庫。
連接之后利用cursor方法創建普通的游標對象。
import pymysql
# 連接數據庫
db = pymysql.connect(host='127.0.0.1', user="root", passwd="cc000822", db="restaurant")
# 使用cursor()方法獲取操作游標
cursor = db.cursor()
從數據庫獲取數據(查)
這里我自己簡單的封裝了一個函數,用來做SELECT查詢操作。該函數有兩個參數,第一個是必選的參數用來指定選擇的范圍,第二個參數是可選參數用來指定選擇條件等。返回值有兩個列表類型的量,分別是結果的標題和結果本身(結果是二維列表),內部設有錯誤捕獲語句,執行出錯時會打印出出錯的SQL語句,方便調試。
def select(table, par=""):
# SQL 查詢語句
data_dict = []
results = []
sql = "SELECT * FROM %s %s;" % \
(table, par)
try:
# 執行SQL語句
cursor.execute(sql)
# 獲取所有記錄列表
results = cursor.fetchall()
data_dict = []
# 打印出標題
for field in cursor.description:
data_dict.append(field[0])
except:
print(sql + "_失敗")
print("Error: unable to fetch data")
return data_dict, results
插入到數據庫(增)
另一個封裝的是插入INSERT函數,這個函數也很簡單,兩個參數分別是要插入的表(或者表中具體的列)和要插入的數據(VALUES后面的內容包括括號)。該函數沒有返回值,若出錯則會在錯誤捕獲語句輸出該語句,並回滾。
def Insert(table, par):
sql = "INSERT INTO %s VALUES %s;" % \
(table, par)
try:
# 執行SQL語句
cursor.execute(sql)
db.commit()
print(sql + "成功")
except:
db.rollback()
print(sql + "_失敗")
print("Error: file to insert")
修改數據(改)
修改數據功能在本程序中沒有統一的格式,故沒有封裝成函數的形式。這里就拿修改商家個人信息舉例(其余的大同小異)。
由於可修改的信息不止一個,每個也都可以選擇是否修改(若要修改就在相應位置填值反之不填),而UPDATE語句的SET之間要用逗號隔開且第一個之前沒有逗號。
所以要做兩個判斷 :
- 判斷每個屬性是否需要被修改,需要的話就在SQL語句加上相應的部分
- 修改當前屬性之前是否有屬性已經被修改過(當前是否是第一個被修改的屬性),若前面都沒有被修改過即當前是第一個被修改的屬性則前面不需要加上逗號否則需要加上逗號。
# 將新信息提交到數據庫
changed = 0
sql_alt = 'UPDATE store SET '
if altered_pwd != "":
sql_alt += " Spass=\'" + altered_pwd + "\'"
changed = 1
if altered_addr != "":
if changed == 1:
sql_alt += ','
sql_alt += " Saddr=\'" + altered_addr + "\'"
changed = 1
if altered_tel != "":
if changed == 1:
sql_alt += ','
sql_alt += " Stel=\'" + altered_tel + "\'"
changed = 1
if altered_name != "":
if changed == 1:
sql_alt += ','
sql_alt += " Sname=\'" + altered_name + "\'"
sql_alt += "WHERE Sno=\'" + user_id + "\';"
try: # 執行SQL語句
cursor.execute(sql_alt)
db.commit()
user_pwd = identify_pwd # 修改本地緩存的密碼
tk.messagebox.showinfo(title="通知", message="修改成功!")
except:
db.rollback()
print(sql_alt + "_失敗")
其它算法設計
涉及金融的修改
涉及到金融的修改全部不直接用UPDATE的方式,而是調用在服務器端設置的三個存儲過程
`alter_cus_money`、`alter_del_money`、`alter_store_money`。
三個存儲過程的功能完全相同,不同之處在於面向的對象以及被調用的場景。
alter_cus_money
在用戶提交訂單並做相應的檢查(余額是否足夠等)之后扣除本次用戶的花費,在用戶充值余額並成功驗證密碼之后增加充值的金額。
alter_store_money
在用戶提交訂單完成之后,商家界面上的列表出現對應的訂單,在商家對訂單點擊我已出餐之后,商家營業額會增加該訂單對應的金額。在商家提現並驗證完密碼做完安全性檢查之后營業額會減去相應的提現金額。
alter_del_money
商家點擊我已出餐之后,配送員界面上未配送訂單會出現,配送員送完餐后點擊我已送達(完成訂單)之后配送員的工資會增加該單對應的配送費會。在配送員點擊提現按鈕,提現並驗證完密碼做完安全性檢查之后營業額會減去相應的提現金額。
顧客提交訂單功能
用戶提交訂單的時候,程序只知道兩個消息 一是提交時刻對應的菜單 二是訂單內所有的商品在菜單中的編號,這兩個商品分別存在以下全局變量中
order_list = set() # 當前購買的
cur_menu = [] # 當前的菜單
由於每個訂單內都有可能包含來自多個商家的多個商品。需要處理的問題是如何正確更新orderr和purchase兩個表,如何分配配送員。
解決方法:
用戶下一次訂單,系統識別出來自哪些商家,並將其按照商家拆分成不同的訂單,每個訂單對應一個商家以及一個配送員,每個訂單只對應來自一個商家的商品。
實際操作過程中只需要遍歷整個列表,並記錄當前下的單對應的商店集合,如果當前商品來自的商店不在該集合內則加入進去並在orderr中插入一條記錄。然后對於每個商品,其對應的店鋪無論是否在集合內(不在的剛才已經新建了,所以最終結果是都在),均要在purchase表中插入一條記錄。
在插入orderr表的時候需要為其分配一個訂單號和一個配送員,所以需要事先獲取訂單表已經存在的最大編號,將新訂單號分配為這個最大編號+1,還要獲取全部在上班的配送員列表,在列表中隨機挑選一個配送員將其信息插入對應的訂單中。
訂單插入orderrd的時候還沒有訂單對應的商品,所以訂單金額和配送費都是初始值。每在purchase表中插入一條記錄時,要在對應的orderr記錄配送費加一以及總金額加上商品價格。這里是用觸發器來實現的,每次在purchase插入新內容之后自動完成這一任務,不再需要程序干預。
當order_list
遍歷結束后訂單的提交也就結束了。
商家和配送員完成訂單功能
在用戶提交一個訂單之后,該訂單的狀態為正在出餐,商家可以在未出餐列表中看到該訂單,商家准備完成后,選擇相應的訂單點擊我已出餐,訂單狀態變為正在配送,配送員在待配送列表中可以看到該訂單,配送員配送完成后點擊我已送達,訂單狀態變為訂單完成。全程都可以在訂單欄看到訂單的狀態。
商家和配送員的完成訂單功能類似,因為程序沿用了客戶端的部分代碼,命名規則也直接沿用了過來, cur_menu,order_list = set()
這兩個變量分別代表當前待處理的訂單列表,和已選擇的訂單編號。所以提交處理訂單時只需要遍歷order_list 列表並找到cur_menu對應編號的訂單,修改狀態即可。
一鍵修改工作狀態
商家和配送員都有切換工作狀態的功能,按下相應按鈕后會在工作/休息之間切換。
商家的狀態決定了其商品是否能在用戶的菜單上顯示,用戶的菜單只會顯示當前在工作狀態的商店的菜品,所以處於休息狀態的商家無法接到新訂單。配送員類似,系統分配新訂單給派送員的時候只會分配給在工作狀態的配送員。
實現方法:
在程序中創建一鍵切換工作狀態后,將其command設置為change_work_state()
函數,在此函數內會先讀取工作狀態(全局變量,每次刷新控件時都會更新一次),然后再根據當前狀態生成UPDATE語句提交到數據庫執行,並將執行的結果反饋在消息框中。
商品管理模塊
這個模塊有查詢商品詳情、新建商品、修改原有商品、刪除商品四個功能。
點擊商品管理后進入該模塊,左側是商品詳情區域,展示所有本店鋪的商品信息。右上方是輸入區,輸入對應功能需要的信息,右下方是功能選擇區域,點擊其中的按鈕會讀取上方對應的信息並做對應的操作。
新建商品功能需要錄入全部的信息並且保證商品ID不重復,價格和庫存不為負值。
修改商品必須填入ID,其它選擇要修改的填寫即可。
刪除商品只需要填入商品ID即可。
感謝支持
