預(yù)約系統(tǒng)源碼拆解與二次開(kāi)發(fā))
簡(jiǎn)介一套基于ThinkPHPMySQLuniappFastAdmin后臺(tái)開(kāi)發(fā)的上門(mén)家政按摩H5小程序與預(yù)約系統(tǒng)源碼面向需要快速落地同城O2O上門(mén)服務(wù)的開(kāi)發(fā)者、外包團(tuán)隊(duì)及產(chǎn)品經(jīng)理可直接部署或二次開(kāi)發(fā)。程序已測(cè)試通過(guò)配套完整包含公眾號(hào)通知、后端主程序、前端uniapp源碼、數(shù)據(jù)庫(kù)SQL及通知音頻文件。資源包共2000個(gè)文件大小66.31MB主要文件類型為865個(gè)JavaScript、546個(gè)Vue組件、350個(gè)Markdown文檔、91個(gè)JSON配置以及HTML、CSS、XML、Shell、SQL、Doc、Python等文件覆蓋前端頁(yè)面、接口交互、數(shù)據(jù)庫(kù)腳本和環(huán)境配置。運(yùn)行環(huán)境需LinuxNginxPHP7.2MySQL5.6并安裝fileinfo、Redis擴(kuò)展系統(tǒng)基于微信生態(tài)需備案域名、認(rèn)證服務(wù)號(hào)、微信支付及小程序賬號(hào)H5指公眾號(hào)H5須在微信中訪問(wèn)。目前已有255人學(xué)習(xí)下載??山柙创a理解FastAdmin權(quán)限管理與uniapp多端開(kāi)發(fā)的完整協(xié)作流程掌握預(yù)約下單、服務(wù)派單、支付回調(diào)、公眾號(hào)通知和音頻提醒等關(guān)鍵模塊的落地實(shí)現(xiàn)是學(xué)習(xí)同城上門(mén)預(yù)約類項(xiàng)目的實(shí)用參考。1. 上門(mén)預(yù)約系統(tǒng)到底解決了什么問(wèn)題一套三端代碼的本地生活服務(wù)樣板做本地生活服務(wù)的人應(yīng)該都有同感用戶端想要小程序順手還能分享H5管理端要能派單、能結(jié)算、能調(diào)整服務(wù)價(jià)格兩頭都不能少。這套基于 thinkphp mysql uniapp 組合的上門(mén)預(yù)約系統(tǒng)把幾個(gè)端一次性湊齊了——前臺(tái)用 uni-app 生成微信小程序和 H5后臺(tái)用 fastadmin 搭好了管理界面服務(wù)端用 ThinkPHP 寫(xiě)接口MySQL 負(fù)責(zé)訂單、技師、用戶、地址這些核心數(shù)據(jù)。對(duì)想快速上線上門(mén)家政、按摩、保潔這類業(yè)務(wù)的團(tuán)隊(duì)來(lái)說(shuō)它省掉的不只是 UI 工作量還有從下單、派單到完工結(jié)算的整條業(yè)務(wù)鏈路。下面按源碼實(shí)際結(jié)構(gòu)拆開(kāi)講。2. 三端架構(gòu)拆解ThinkPHP、FastAdmin 與 uni-app 如何各司其職這套系統(tǒng)的架構(gòu)一句話能說(shuō)清一個(gè)基于 ThinkPHP 的 FastAdmin 應(yīng)用提供全部后端能力用戶端是獨(dú)立的 uni-app 工程數(shù)據(jù)統(tǒng)一存在 MySQL 里。FastAdmin 跑在同一個(gè) PHP 應(yīng)用上通過(guò) admin 模塊提供后臺(tái)用戶端走 api 模塊兩者共用同一套模型和配置。這種一個(gè)應(yīng)用兩套入口的結(jié)構(gòu)是 FastAdmin 系源碼最常見(jiàn)的部署形態(tài)好處是二次開(kāi)發(fā)時(shí)不用維護(hù)兩套后端壞處是路由和權(quán)限要分清楚別讓前端接口和管理頁(yè)面混在一起。2.1 為什么是這套組合二次開(kāi)發(fā)視角的選型邏輯先說(shuō)結(jié)論這套組合選的不是最強(qiáng)的技術(shù)而是最省人力的技術(shù)。FastAdmin 基于 ThinkPHP自帶管理員登錄、權(quán)限節(jié)點(diǎn)、通用列表和表單生成后臺(tái)管理頁(yè)基本不用從零寫(xiě)uni-app 用 Vue 語(yǔ)法寫(xiě)一套代碼編譯輸出微信小程序和 H5對(duì)上門(mén)預(yù)約這種重分享、輕安裝的業(yè)務(wù)剛好合適thinkphp mysql 的部署資料多一臺(tái) Nginx 就能撐起一個(gè)小型平臺(tái)。從改造角度看這套選型還有一個(gè)隱藏優(yōu)點(diǎn)api 模塊和 admin 模塊在同一個(gè)應(yīng)用里訂單模型在 application/common/model 下是前后端共用的。后面加字段、改狀態(tài)、接支付只需要改一處模型不會(huì)出現(xiàn)后臺(tái)改完了接口沒(méi)跟上的問(wèn)題。等業(yè)務(wù)量上來(lái)接口和后臺(tái)也可以拆成兩個(gè)應(yīng)用分別部署目錄結(jié)構(gòu)已經(jīng)留好了拆分邊界。2.2 目錄結(jié)構(gòu)后端、后臺(tái)、前端各自的主力目錄與用途源碼解壓后主要看兩部分PHP 后端工程和 uniapp 前端工程。下面這張表把最常用的目錄列出來(lái)其他目錄大多可以暫時(shí)忽略。關(guān)注點(diǎn)目錄說(shuō)明站點(diǎn)入口public/Web 根目錄必須指向這里下有 index.php 與 uploads后臺(tái)控制器application/admin/controller/FastAdmin 管理端按訂單、服務(wù)、技師等模塊分文件前臺(tái)接口application/api/controller/H5 / 小程序請(qǐng)求的控制器Order、Service、Technician 等數(shù)據(jù)層application/common/model/訂單模型、技師模型前后端共用數(shù)據(jù)庫(kù)配置application/database.php數(shù)據(jù)庫(kù)連接參數(shù)、表前綴前端頁(yè)面uniapp/pages/首頁(yè)、服務(wù)列表、確認(rèn)訂單、訂單詳情等頁(yè)面前端公共uniapp/common/request.js 等公共方法幾個(gè)重點(diǎn)目錄展開(kāi)說(shuō)。application/api/controller 是用戶端實(shí)際打到的接口層FastAdmin 的 API 模塊路由通常是 index.php?s/api/控制器/方法前端請(qǐng)求路徑會(huì)拼成類似 /api/order/create 的形式。public 下的 uploads 存放后臺(tái)上傳的服務(wù)圖、技師頭像站點(diǎn)根目錄必須指向 public否則上傳文件會(huì)落到外部目錄導(dǎo)致前端加載不到。uniapp 前端里pages.json 控制頁(yè)面路由和 tabBarmanifest.json 是小程序 appid、H5 域名這些配置的集中地聯(lián)調(diào)時(shí)改的最多的就是這兩個(gè)文件加 common/request.js。2.3 數(shù)據(jù)庫(kù)設(shè)計(jì)訂單表、技師表、服務(wù)表怎么配合表前綴默認(rèn)是 fa_安裝時(shí)也可以改。核心就三張業(yè)務(wù)表加一張用戶表。服務(wù)項(xiàng)目表結(jié)構(gòu)大致是這樣字段類型說(shuō)明idint主鍵namevarchar服務(wù)名稱如保開(kāi)關(guān)、推拿pricedecimal(10,2)單價(jià)單位元unitvarchar計(jì)費(fèi)單位次 / 小時(shí)service_minutesint預(yù)計(jì)服務(wù)時(shí)長(zhǎng)category_idint所屬分類statustinyint1 上架0 下架技師表 fa_technician 除了姓名、頭像、手機(jī)號(hào)之外最重要的是 lng、lat、server_radius 三個(gè)字段。lng 和 lat 是技師當(dāng)前位置坐標(biāo)server_radius 是服務(wù)半徑單位公里派單時(shí)用這兩個(gè)字段做距離篩選。訂單表 fa_order 則是業(yè)務(wù)核心包含 order_no 訂單號(hào)、user_id、service_id、technician_id、address、address_coordinate、amount、status、serve_time、pay_time、finish_time、remark 和創(chuàng)建時(shí)間。address_coordinate 字段存的是經(jīng)度,緯度逗號(hào)拼接的字符串派單 SQL 會(huì)直接用它做計(jì)算。訂單狀態(tài) status 是這套源碼的靈魂常見(jiàn)的定義是 1 待支付、2 已支付待派單、3 已派單、4 服務(wù)中、5 已完成、6 已取消。價(jià)格字段統(tǒng)一 decimal(10,2)不要用 float否則累計(jì)對(duì)賬時(shí)會(huì)出現(xiàn)精度問(wèn)題。這些字段定義是理解后面核心代碼的前提。3. 核心代碼鏈路一次預(yù)約從下單到派單的完整實(shí)現(xiàn)把用戶操作到后臺(tái)派單的過(guò)程串一遍用戶選服務(wù)填地址和時(shí)間提交訂單后臺(tái)按距離找技師技師接單開(kāi)始服務(wù)完成訂單。對(duì)應(yīng)到代碼上是三個(gè)核心文件前端的請(qǐng)求封裝、后端的下單控制器、派單控制器。下面按這條鏈路逐個(gè)拆。3.1 統(tǒng)一請(qǐng)求封裝token 與 401 跳轉(zhuǎn)都收在一處所有前端請(qǐng)求走同一個(gè)封裝函數(shù)是這類項(xiàng)目最基礎(chǔ)也最重要的約定。uniapp 工程里 common/request.js 負(fù)責(zé)統(tǒng)一拼接域名、帶 token、處理登錄過(guò)期。// uniapp/common/request.js const BASE_URL https://api.yourdomain.com/api; function request(path, data {}, method GET) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL path, method, data, header: { token: uni.getStorageSync(token) || , Content-Type: application/json }, success: res { // 后端統(tǒng)一返回 code 字段401 表示登錄態(tài)失效 if (res.data.code 401) { uni.removeStorageSync(token); uni.navigateTo({ url: /pages/login/login }); reject(res.data); return; } resolve(res.data); }, fail: reject }); }); } export default request;邏輯說(shuō)明每個(gè)請(qǐng)求自動(dòng)從本地緩存拿 token 放進(jìn) header后端靠這個(gè) token 識(shí)別用戶身份一旦接口返回 401說(shuō)明 token 失效或未登錄前端統(tǒng)一清理本地狀態(tài)并跳轉(zhuǎn)登錄頁(yè)。BASE_URL 是聯(lián)調(diào)時(shí)第一個(gè)要替換的地方后續(xù)所有頁(yè)面都通過(guò) import request 來(lái)發(fā)請(qǐng)求不要分散在各頁(yè)面里用 uni.request 裸調(diào)。參數(shù)說(shuō)明path 是接口路徑data 是請(qǐng)求體method 默認(rèn) GETPOST 接口調(diào)用時(shí)顯式傳入第三個(gè)參數(shù)。后端要求的 token 字段名是 token如果你的項(xiàng)目改成了 Authorization這里和 FastAdmin 的 API 驗(yàn)權(quán)配置要同步改否則會(huì)出現(xiàn)登錄成功但業(yè)務(wù)接口全部未登錄的問(wèn)題。3.2 創(chuàng)建訂單接口必傳參數(shù)、狀態(tài)初值、事務(wù)用戶點(diǎn)提交訂單時(shí)前端把服務(wù) ID、地址、期望上門(mén)時(shí)間、備注 POST 到 api/order/create。后臺(tái)控制器的核心邏輯是校驗(yàn)服務(wù)是否存在組裝訂單數(shù)據(jù)寫(xiě)庫(kù)并返回訂單 ID。// application/api/controller/Order.php public function create() { $uid $this-user_id; $service_id $this-request-post(service_id); $address $this-request-post(address); $serve_time $this-request-post(serve_time); $remark $this-request-post(remark, ); $service ServiceModel::get($service_id); if (!$service || $service-status ! 1) { $this-error(服務(wù)項(xiàng)目不存在或已下架); } $order_data [ order_no date(YmdHis) . mt_rand(1000, 9999), user_id $uid, service_id $service_id, amount $service-price, address $address, serve_time $serve_time, remark $remark, status 1 // 待支付 ]; Db::startTrans(); try { $order OrderModel::create($order_data); Db::commit(); $this-success(下單成功, [order_id $order-id]); } catch (\Exception $e) { Db::rollback(); $this-error(下單失敗請(qǐng)重試); } }邏輯說(shuō)明先查服務(wù)表確認(rèn)服務(wù)存在且在上架狀態(tài)再組裝訂單數(shù)據(jù)。order_no 用時(shí)間加隨機(jī)數(shù)生成保證不會(huì)重復(fù)amount 直接取服務(wù)價(jià)格不讓前端傳價(jià)格這是防止用戶篡改金額的基本做法。status 初始化為 1 待支付。事務(wù)在這里雖然沒(méi)有跨表寫(xiě)入但后續(xù)如果加優(yōu)惠券核銷、加積分記錄事務(wù)就非常必要了所以保留這個(gè)結(jié)構(gòu)。參數(shù)說(shuō)明service_id 和 address 是必傳serve_time 是預(yù)約時(shí)間remark 可選。前端傳價(jià)格一律不接收。下單成功后返回 order_id前端再用這個(gè)訂單 ID 喚起支付或跳轉(zhuǎn)訂單詳情。如果想加提交訂單時(shí)鎖定技師在 order_data 里加 technician_id 字段即可第 6 章會(huì)專門(mén)講這個(gè)改造。3.3 附近技師查詢一段基于經(jīng)緯度的派單 SQL派單是整個(gè)上門(mén)預(yù)約系統(tǒng)的核心難點(diǎn)。這套源碼里的做法很直接以訂單地址的經(jīng)緯度為中心計(jì)算技師表里每個(gè)在崗技師的直線距離過(guò)濾出服務(wù)半徑內(nèi)的技師按距離升序返回。// application/api/controller/Assign.php public function findNearby() { $order_id $this-request-post(order_id); $order OrderModel::get($order_id); if (!$order) { $this-error(訂單不存在); } list($lng, $lat) explode(,, $order-address_coordinate); $list TechnicianModel::where(status, 1) -field(id, name, avatar, server_radius, (6378.138 * 2 * ASIN(SQRT(POW(SIN(({$lat} * PI() / 180 - lat * PI() / 180) / 2), 2) COS({$lat} * PI() / 180) * COS(lat * PI() / 180) * POW(SIN(({$lng} * PI() / 180 - lng * PI() / 180) / 2), 2)))) AS distance) -having(distance server_radius) -order(distance) -select(); $this-success(附近技師, $list); }邏輯說(shuō)明這段 SQL 用的是 Haversine 公式6378.138 是地球半徑公里數(shù)distance 的單位是公里。查詢條件有兩個(gè)技師狀態(tài)為 1在崗且算出來(lái)的距離小于技師自己設(shè)定的服務(wù)半徑。結(jié)果按距離從近到遠(yuǎn)排前端拿到的就是一個(gè)可選的技師列表。參數(shù)說(shuō)明address_coordinate 字段必須是經(jīng)度,緯度格式如果為空這條 SQL 會(huì)直接報(bào)錯(cuò)。常見(jiàn)的兜底做法是退化成按城市加狀態(tài)篩選先在地址表里解析出城市字段然后查 city_id 匹配的技師。這個(gè)兜底建議在改造時(shí)加上能避免訂單因?yàn)槿弊鴺?biāo)卡死在派單環(huán)節(jié)。3.4 FastAdmin 后臺(tái)訂單列表與狀態(tài)切換怎么配置后端接口就緒后管理后臺(tái)的訂單列表不需要手寫(xiě)頁(yè)面FastAdmin 提供了命令行生成器。在應(yīng)用根目錄執(zhí)行以下命令就能給 fa_order 表生成一套標(biāo)準(zhǔn)的后臺(tái)增刪改查php think crud -t fa_order -u 1執(zhí)行后application/admin 下會(huì)出現(xiàn) Order 控制器和視圖文件后臺(tái)菜單自動(dòng)掛到 admin/order 下。列表展示哪些字段、字段怎么格式化由 application/admin/view/order/index.html 里對(duì)應(yīng)的 JS 配置 control 決定。訂單狀態(tài)切換是運(yùn)營(yíng)最頻繁的操作對(duì)應(yīng)的代碼邏輯通常長(zhǎng)這樣// application/admin/view/order/index.html 對(duì)應(yīng)的事件綁定 $(.btn-change-status).on(click, function () { var id $(this).data(id); $.ajax({ url: order/changeStatus, data: { id: id, status: 3 }, // 3 表示已派單 success: function (res) { if (res.code 1) { window.location.reload(); } } }); });邏輯說(shuō)明changeStatus 是后臺(tái)控制器里自定義的方法接收訂單 ID 和目標(biāo)狀態(tài)更新后刷新列表。這套結(jié)構(gòu)的好處是每個(gè)狀態(tài)流轉(zhuǎn)動(dòng)作都可以掛獨(dú)立的校驗(yàn)比如從服務(wù)中改成已完成時(shí)強(qiáng)制填寫(xiě)完結(jié)金額從已派單改成已取消時(shí)記錄取消原因。參數(shù)說(shuō)明data 里的 id 是訂單主鍵status 是目標(biāo)狀態(tài)碼。后臺(tái)控制器里對(duì)應(yīng)接收這兩個(gè)參數(shù)并在操作前用管理員權(quán)限做一次驗(yàn)權(quán)防止越權(quán)改單。4. 本地搭建與運(yùn)行從空服務(wù)器到 H5小程序跑通這一章目標(biāo)很直接把這套源碼在你自己的機(jī)器上跑起來(lái)。先說(shuō)后端初始化再說(shuō)前端聯(lián)調(diào)最后給一個(gè)三端已經(jīng)連通的判斷方法。整個(gè)過(guò)程中 90% 的問(wèn)題集中在配置和路徑上代碼本身基本不用動(dòng)。4.1 后端初始化導(dǎo)庫(kù)、改配置、配偽靜態(tài)環(huán)境準(zhǔn)備按 FastAdmin 的常規(guī)要求來(lái)PHP 7.1 到 7.4 最穩(wěn)MySQL 5.7 以上Nginx 或 Apache 都行。先把數(shù)據(jù)庫(kù)建好再把源碼自帶的 SQL 文件導(dǎo)入。SQL 文件一般在根目錄或 sql 目錄下不同發(fā)行版位置不同先找一下。mysql -uroot -p -e create database fastadmin_home default charset utf8mb4; mysql -uroot -p fastadmin_home install.sql兩條命令的含義第一條創(chuàng)建名為 fastadmin_home 的數(shù)據(jù)庫(kù)指定 utf8mb4 字符集第二條把 install.sql 里的表結(jié)構(gòu)和初始數(shù)據(jù)導(dǎo)入這個(gè)庫(kù)。導(dǎo)入成功后會(huì)看到 fa_user、fa_order、fa_technician 等表。接著修改 application/database.php把數(shù)據(jù)庫(kù)連接參數(shù)換成你自己的。需要改的字段是 hostname、database、username、password、prefix。prefix 默認(rèn) fa_如果導(dǎo)入的 SQL 里表前綴不是 fa_這里必須改成一致的否則后臺(tái)登錄會(huì)報(bào)表不存在。最后是偽靜態(tài)配置。FastAdmin 的路由是 pathinfo 風(fēng)格Nginx 下站點(diǎn)根目錄要指向 public并加上 rewrite。配置示例server { listen 80; server_name localhost; root /path/to/項(xiàng)目根目錄/public; index index.php; location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s$1 last; } } }提示根目錄指錯(cuò)是最常見(jiàn)的啟動(dòng)失敗原因。一定要指到 public 目錄不是項(xiàng)目根目錄。配置完成后訪問(wèn) http://localhost/index.php?s/admin 進(jìn)入后臺(tái)默認(rèn)管理員賬號(hào)通常為 admin密碼 admin 或 123456。上線前第一件事就是改掉默認(rèn)密碼不要在這上面偷懶。4.2 前端聯(lián)調(diào)H5 與微信小程序?qū)雍蠖说牡胤讲灰粯忧岸斯こ逃?HBuilderX 打開(kāi) uniapp 目錄第一步修改 common/request.js 里的 BASE_URL。如果后端跑在本機(jī)H5 開(kāi)發(fā)模式建議用代理解決跨域不用改 BASE_URL 的域名。manifest.json 的 h5 節(jié)點(diǎn)配置本地代理// uniapp manifest.json 的開(kāi)發(fā)配置片段 h5: { devServer: { proxy: { /api: { target: http://127.0.0.1:8080, changeOrigin: true } } } }邏輯說(shuō)明開(kāi)發(fā)環(huán)境下前端請(qǐng)求 /api 開(kāi)頭的地址會(huì)自動(dòng)轉(zhuǎn)發(fā)到本機(jī) 8080 端口的 PHP 應(yīng)用繞開(kāi)瀏覽器跨域限制。生產(chǎn)環(huán)境 H5 直接配 HTTPS 域名不需要代理。微信小程序和 H5 的差別在域名校驗(yàn)。微信開(kāi)發(fā)者工具里默認(rèn)會(huì)校驗(yàn) request 合法域名本地聯(lián)調(diào)時(shí)可以在詳情 → 本地設(shè)置里勾選不校驗(yàn)合法域名。但真機(jī)預(yù)覽和體驗(yàn)版必須在小程序后臺(tái)把 API 域名加入 request 合法域名而且要求是已備案的 HTTPS 域名。這點(diǎn)后面避坑章還會(huì)再提。4.3 三端已連通的判斷方法跑通后怎么確認(rèn)鏈路是通的按三條線自檢第一打開(kāi)后端 runtime/log 目錄下的日志文件看請(qǐng)求是否進(jìn)來(lái)、在哪一步停下第二用接口調(diào)試工具先調(diào)登錄接口拿 token再帶 token 調(diào)一次訂單列表接口接口能通說(shuō)明 PHP 應(yīng)用本身沒(méi)問(wèn)題第三在后臺(tái)創(chuàng)建一個(gè)測(cè)試訂單然后查 MySQL 的 fa_order 表能看到新記錄就說(shuō)明數(shù)據(jù)鏈路完整。這三條自檢順序固定日志確認(rèn)請(qǐng)求到達(dá)接口確認(rèn)邏輯正確數(shù)據(jù)庫(kù)確認(rèn)落庫(kù)成功。按這個(gè)順序排查能把前端問(wèn)題和后端問(wèn)題快速分開(kāi)避免在兩端來(lái)回猜。5. 避坑記錄搭建與聯(lián)調(diào)中最常踩的五個(gè)坑這一章是實(shí)際操作里反復(fù)遇到的坑按現(xiàn)象到原因再到解決展開(kāi)。每一條都對(duì)應(yīng)一個(gè)具體的排查路徑希望你看完之后遇到同類問(wèn)題能直接定位。5.1 小程序請(qǐng)求全部失敗url not in domain list現(xiàn)象微信開(kāi)發(fā)者工具里所有 uni.request 請(qǐng)求都報(bào) request:fail url not in domain list頁(yè)面數(shù)據(jù)全空。原因小程序平臺(tái)對(duì) request 接口有域名白名單限制。開(kāi)發(fā)者工具默認(rèn)開(kāi)啟合法域名校驗(yàn)而你的 API 域名沒(méi)在小程序后臺(tái)配置或者開(kāi)發(fā)者工具里的不校驗(yàn)合法域名選項(xiàng)沒(méi)勾上。解決本地聯(lián)調(diào)階段在微信開(kāi)發(fā)者工具的詳情 → 本地設(shè)置里勾選不校驗(yàn)合法域名真機(jī)預(yù)覽和發(fā)布前到微信公眾平臺(tái)把 API 域名加入 request 合法域名。注意域名必須是 HTTPS、已備案且協(xié)議和域名要完全一致http 和 https 不能混著填。5.2 后臺(tái)圖片上傳成功前端加載不出來(lái)現(xiàn)象FastAdmin 后臺(tái)上傳的服務(wù)圖片、技師頭像顯示正常但小程序和 H5 頁(yè)面請(qǐng)求這些圖片時(shí) 404 或空白。原因上傳文件默認(rèn)落在 public/uploads 目錄正確訪問(wèn)路徑是 /uploads/xxx.jpg。但很多部署把站點(diǎn)根目錄指到了項(xiàng)目根目錄導(dǎo)致上傳文件實(shí)際在項(xiàng)目根目錄以外或路徑不對(duì)前端拼接的 URL 自然打不開(kāi)。解決先把 Nginx 站點(diǎn)根目錄固定到 public這是根本前端圖片地址用相對(duì)路徑 /uploads/xxx.jpg不要在前端寫(xiě)死帶域名和端口號(hào)的絕對(duì)地址避免域名一換就大面積裂圖。5.3 登錄成功但其他接口全部提示未登錄現(xiàn)象登錄接口返回 token 正常前端也存進(jìn)了緩存但一點(diǎn)進(jìn)訂單詳情就跳回登錄頁(yè)打開(kāi)接口返回 401 未登錄。原因前后端 token 字段名不一致。前端 header 里發(fā)送的是 token后端 API 驗(yàn)權(quán)默認(rèn)也從 header 的 token 取如果某一端改成了 Authorization或者大小寫(xiě)不一致驗(yàn)權(quán)就會(huì)失敗。另一種情況是 token 本身已過(guò)期后端正確返回 401前端統(tǒng)一跳轉(zhuǎn)看起來(lái)像沒(méi)登錄上。解決找到后端 API 驗(yàn)權(quán)的入口確認(rèn)它從哪個(gè) header 取 token把前端 request.js 的 header 改成一致。同時(shí)在登錄接口里檢查 token 有效期配置開(kāi)發(fā)階段可以把過(guò)期時(shí)間調(diào)長(zhǎng)一點(diǎn)減少干擾。5.4 SQL 導(dǎo)入失敗字符集與版本不匹配現(xiàn)象導(dǎo)入 install.sql 時(shí)報(bào)錯(cuò)常見(jiàn)信息有 Index column size too large 或 Unknown collation: utf8mb4_unicode_ci。原因SQL 文件是按 MySQL 5.7 和 utf8mb4 生成的你本地 MySQL 版本過(guò)低或者建庫(kù)時(shí)沒(méi)有指定字符集導(dǎo)致表結(jié)構(gòu)里的索引和排序規(guī)則不被支持。解決建庫(kù)時(shí)顯式指定 utf8mb4命令用 create database xxx default charset utf8mb4把 MySQL 升級(jí)到 5.7 以上。如果還有報(bào)錯(cuò)檢查 sql_mode 是不是嚴(yán)格模式臨時(shí)可以關(guān)掉 strict mode 再導(dǎo)入。5.5 訂單狀態(tài)對(duì)不上前端永遠(yuǎn)顯示待支付現(xiàn)象后臺(tái)訂單已經(jīng)是已支付用戶端卻一直顯示待支付狀態(tài)名和實(shí)際流轉(zhuǎn)對(duì)不上。原因訂單狀態(tài)枚舉的前后端定義不一致。后臺(tái)代碼把 1 定義為待支付前端頁(yè)面把 0 定義為待支付兩邊沒(méi)有共用同一份狀態(tài)字典。解決在前后端各維護(hù)一份公共狀態(tài)字典后端在 common 常量里定義前端在 common/status.js 里定義字段順序和數(shù)值必須完全一致。凡是展示狀態(tài)的地方都從字典取值禁止在頁(yè)面里寫(xiě)死狀態(tài)數(shù)字。改完順手把 fa_order 表里歷史數(shù)據(jù)的 status 也校正一遍。6. 二次開(kāi)發(fā)實(shí)戰(zhàn)把后臺(tái)派單改成用戶自選技師最后做一個(gè)最典型的業(yè)務(wù)改造把默認(rèn)的后臺(tái)指派技師改成用戶自己選技師。這個(gè)改動(dòng)覆蓋數(shù)據(jù)庫(kù)、接口、前端三處做完之后這套源碼的骨架你基本就摸清了。6.1 先鎖定改動(dòng)范圍要改的地方總共三處加一個(gè)字段。數(shù)據(jù)庫(kù)給 fa_order 表加 technician_id 字段存用戶選的技師 ID后端創(chuàng)建訂單接口加一個(gè)可選參數(shù) technician_id并在傳值時(shí)把訂單狀態(tài)改成待技師接單前端確認(rèn)訂單頁(yè)加一個(gè)技師選擇列表展示附近技師和距離。后臺(tái)派單流程保留作為無(wú)人選擇時(shí)的兜底。ALTER TABLE fa_order ADD COLUMN technician_id INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 用戶自選技師ID AFTER service_id;邏輯說(shuō)明technician_id 默認(rèn) 0 表示未指定非 0 表示用戶指定。訂單詳情查詢時(shí)聯(lián)查 fa_technician 表就能拿到技師姓名和頭像。6.2 前后端的具體改法前端在用戶選完服務(wù)后調(diào)一次附近技師接口把結(jié)果渲染成可選列表。距離字段在后端返回時(shí)單位是公里展示前格式化一下會(huì)更友好。// 預(yù)約確認(rèn)頁(yè)的技師選擇邏輯 async function loadTechnicians(serviceId) { const res await request(/order/canAssign, { service_id: serviceId }, POST); const list res.data.map(t ({ ...t, distanceText: t.distance 1 ? t.distance.toFixed(2) km : Math.round(t.distance * 1000) m })); this.technicians list; }邏輯說(shuō)明canAssign 接口內(nèi)部就是第 3 章那段 Haversine 查詢只是入口從訂單 ID 換成了服務(wù) ID。前端拿到列表后用戶點(diǎn)選某個(gè)技師就把技師 ID 存進(jìn)表單提交訂單時(shí)隨請(qǐng)求體一起發(fā)送。后端在創(chuàng)建訂單接口里加一段判斷$technician_id $this-request-post(technician_id, 0); if ($technician_id) { $tech TechnicianModel::get($technician_id); if (!$tech || $tech-status ! 1) { $this-error(技師不可用請(qǐng)重新選擇); } $order_data[technician_id] $technician_id; $order_data[status] 2; // 待技師接單原 2 是已支付待派單這里沿用 2 }邏輯說(shuō)明傳了 technician_id 才會(huì)校驗(yàn)技師狀態(tài)并把訂單狀態(tài)置為待技師接單不傳則維持原來(lái)的待支付流程由后臺(tái)派單。這兩個(gè)分支互不影響。6.3 改造后的驗(yàn)證驗(yàn)證要覆蓋兩條路徑在 FastAdmin 后臺(tái)給某技師填上經(jīng)緯度用前端選該技師下單查 fa_order 表確認(rèn) technician_id 有值、status 為 2再走一遍不選技師的舊流程確認(rèn)后臺(tái)派單仍然正常。兩條路徑都通改造才算完成。拆這類源碼最值錢(qián)的地方其實(shí)不是某個(gè)接口怎么寫(xiě)而是狀態(tài)和字段的約定怎么貫穿三端。我這次改的時(shí)候第一件事不是寫(xiě)代碼而是先翻 fa_order 的 status 注釋和前端的公共字典把待派單、待接單的定義對(duì)齊才動(dòng)手。從那以后我接手任何 FastAdmin 項(xiàng)目第一件事都是先導(dǎo)出數(shù)據(jù)庫(kù)字典再碰控制器這個(gè)習(xí)慣幫我少走了不少?gòu)澛?。希望這份拆解和改造思路對(duì)你也有用。本文還有配套的精品資源點(diǎn)擊獲取