券到返現(xiàn)全鏈路實(shí)戰(zhàn))
簡介這份資源是面向本地外賣商家與小程序開發(fā)者的霸王餐系統(tǒng)源碼圍繞美團(tuán)、餓了么等主流外賣場景提供商家入駐、霸王餐發(fā)布、優(yōu)惠券設(shè)置與用戶評(píng)價(jià)反饋等核心模塊幫助商家以免費(fèi)體驗(yàn)活動(dòng)吸引新客、沉淀口碑也方便開發(fā)者二次搭建本地生活服務(wù)平臺(tái)。壓縮包為zip格式整體約2.06MB文件總數(shù)暫未提供明細(xì)核心目錄以bawangcan命名通常包含程序邏輯、商家管理、活動(dòng)發(fā)布與兌換機(jī)制、評(píng)論系統(tǒng)及數(shù)據(jù)庫存儲(chǔ)等關(guān)鍵內(nèi)容便于按模塊查閱與部署。目前已有1795人學(xué)習(xí)下載適合具備一定小程序或后端開發(fā)基礎(chǔ)、希望快速落地外賣營銷系統(tǒng)的讀者參考。通過源碼可了解霸王餐活動(dòng)的完整業(yè)務(wù)閉環(huán)包括商家認(rèn)證、活動(dòng)配置、用戶參與與消費(fèi)后評(píng)價(jià)等流程對(duì)理解本地外賣平臺(tái)的運(yùn)營與技術(shù)實(shí)現(xiàn)有實(shí)際幫助。1. 省錢兄外賣霸王餐程序一套能跑通的霸王餐小程序源碼拆解外賣霸王餐這個(gè)玩法本質(zhì)上是用返現(xiàn)邏輯把「點(diǎn)外賣」變成「先付后返」的閉環(huán)。省錢兄這套源碼就是干這個(gè)的用戶在小程序里領(lǐng)券、下單、上傳訂單截圖后臺(tái)審核通過后按比例返現(xiàn)到余額。它適合兩類人——想快速搭一個(gè)本地生活返現(xiàn)平臺(tái)的創(chuàng)業(yè)者以及想拿一套完整小程序全棧項(xiàng)目練手的前后端開發(fā)者。技術(shù)棧是典型的小程序 服務(wù)端組合前端用微信小程序原生或 uni-app后端常見的是 Node.js 或 PHP 配 MySQL管理端另起一個(gè) Web 后臺(tái)。整套東西不復(fù)雜但鏈路完整從登錄授權(quán)、券碼管理、訂單提交到返現(xiàn)審核都有對(duì)應(yīng)模塊拿來改比從零寫省太多事。2. 項(xiàng)目架構(gòu)與目錄結(jié)構(gòu)先看清這套小程序怎么分層2.1 整體技術(shù)選型與分層思路這套霸王餐程序走的是「小程序端 API 服務(wù) 管理后臺(tái)」三層結(jié)構(gòu)。小程序端負(fù)責(zé)用戶交互API 服務(wù)處理業(yè)務(wù)邏輯和數(shù)據(jù)庫讀寫管理后臺(tái)給運(yùn)營人員審核訂單、配置返現(xiàn)比例、管理券碼。數(shù)據(jù)庫用 MySQL 存用戶、訂單、券碼、返現(xiàn)記錄這幾張核心表緩存層常見做法是加一層 Redis 扛住券碼領(lǐng)取的并發(fā)。為什么這么分因?yàn)榘酝醪偷暮诵拿茉凇阜惮F(xiàn)審核」——用戶提交訂單后不能立刻返錢得有人工或自動(dòng)審核環(huán)節(jié)。如果全塞在小程序端做邏輯會(huì)亂成一鍋粥。拆成三層后小程序只管展示和提交API 管狀態(tài)流轉(zhuǎn)后臺(tái)管審核職責(zé)清晰后面改返現(xiàn)規(guī)則也不用動(dòng)前端。技術(shù)棧上如果你拿到的版本是 Node.js 后端大概率是 Express 或 Koa 配 Sequelize/TypeORM如果是 PHP 版本一般是 ThinkPHP 或 Laravel。前端小程序部分原生寫法用 WXML WXSS JSuni-app 版本則是 Vue 語法編譯到多端。兩種都能跑選哪個(gè)看你團(tuán)隊(duì)熟悉什么。2.2 目錄結(jié)構(gòu)逐層說明一個(gè)典型的目錄長這樣以 Node.js 原生小程序?yàn)槔齪roject-root/ ├── miniprogram/ # 小程序端 │ ├── pages/ │ │ ├── index/ # 首頁券碼列表、領(lǐng)取入口 │ │ ├── order/ # 下單頁跳轉(zhuǎn)外賣平臺(tái)、填訂單號(hào) │ │ ├── submit/ # 提交返現(xiàn)上傳截圖、填金額 │ │ ├── mine/ # 個(gè)人中心余額、返現(xiàn)記錄 │ │ └── login/ # 授權(quán)登錄 │ ├── components/ # 公共組件券卡片、訂單狀態(tài)標(biāo)簽 │ ├── utils/ # 請(qǐng)求封裝、格式化工具 │ └── app.js # 全局配置、登錄態(tài)管理 ├── server/ # API 服務(wù) │ ├── controllers/ # 業(yè)務(wù)邏輯user、order、coupon、cashback │ ├── models/ # 數(shù)據(jù)模型User、Order、Coupon、Cashback │ ├── routes/ # 路由定義 │ ├── middlewares/ # 鑒權(quán)、日志、錯(cuò)誤處理 │ └── app.js # 服務(wù)入口 ├── admin/ # 管理后臺(tái)Vue/React │ ├── src/ │ │ ├── views/ # 訂單審核、券碼管理、用戶列表 │ │ └── api/ # 后臺(tái)接口封裝 │ └── package.json └── config/ # 數(shù)據(jù)庫、Redis、微信配置這個(gè)結(jié)構(gòu)里miniprogram/pages下的每個(gè)目錄對(duì)應(yīng)一個(gè)完整頁面server/controllers按業(yè)務(wù)域拆分admin是獨(dú)立的前端工程。拿到源碼后先看config目錄數(shù)據(jù)庫連接、微信 AppID、返現(xiàn)比例這些都在里面改完就能跑。2.3 核心數(shù)據(jù)表設(shè)計(jì)霸王餐程序的數(shù)據(jù)模型不復(fù)雜但有幾張表必須設(shè)計(jì)對(duì)表名關(guān)鍵字段說明usersid, openid, balance, total_cashback用戶表balance 是返現(xiàn)余額couponsid, platform, title, link, status券碼表platform 區(qū)分美團(tuán)/餓了么ordersid, user_id, coupon_id, order_no, amount, status訂單表status 標(biāo)記待審核/已返現(xiàn)/駁回cashbacksid, order_id, user_id, ratio, amount, created_at返現(xiàn)記錄ratio 是返現(xiàn)比例orders表的status字段是整個(gè)流程的開關(guān)用戶提交后是pending后臺(tái)審核通過變approved并觸發(fā)返現(xiàn)駁回變r(jià)ejected。這個(gè)狀態(tài)機(jī)設(shè)計(jì)好了后面加自動(dòng)審核、加返現(xiàn)階梯都容易。3. 核心模塊代碼拆解從領(lǐng)券到返現(xiàn)的完整鏈路3.1 用戶授權(quán)登錄與 openid 獲取小程序登錄繞不開wx.login換 openid 這一步。常見做法是前端拿到 code 傳給后端后端調(diào)微信接口換 openid 和 session_key然后生成自定義登錄態(tài)返回給前端。// miniprogram/utils/auth.js // 前端調(diào)用 wx.login 獲取 code傳給后端換 token function login() { return new Promise((resolve, reject) { wx.login({ success: (res) { if (res.code) { // 把 code 發(fā)給后端后端換 openid 并返回 token wx.request({ url: ${BASE_URL}/api/user/login, method: POST, data: { code: res.code }, success: (resp) { // token 存本地后續(xù)請(qǐng)求帶上 wx.setStorageSync(token, resp.data.token); resolve(resp.data); }, fail: reject }); } else { reject(new Error(wx.login 失敗)); } }, fail: reject }); }); }后端對(duì)應(yīng)處理// server/controllers/user.js const axios require(axios); async function login(req, res) { const { code } req.body; // 調(diào)微信接口換 openid const { data } await axios.get(https://api.weixin.qq.com/sns/jscode2session, { params: { appid: config.wx.appid, secret: config.wx.secret, js_code: code, grant_type: authorization_code } }); if (!data.openid) return res.status(400).json({ msg: 登錄失敗 }); // 查用戶是否存在不存在則注冊(cè) let user await User.findOne({ where: { openid: data.openid } }); if (!user) { user await User.create({ openid: data.openid, balance: 0 }); } // 生成 tokenJWT 或自定義 const token jwt.sign({ userId: user.id }, config.jwtSecret, { expiresIn: 7d }); res.json({ token, userId: user.id }); }這里的關(guān)鍵參數(shù)appid和secret在config里配js_code就是前端傳來的 code。注意 code 只能用一次且五分鐘內(nèi)有效所以別在客戶端緩存 code。token 有效期設(shè) 7 天是常見做法太長不安全太短用戶老要重新登錄。3.2 券碼領(lǐng)取與并發(fā)控制券碼領(lǐng)取是第一個(gè)容易翻車的地方。如果同一張券被多人同時(shí)領(lǐng)庫存扣減會(huì)出問題。常見做法是用 Redis 做原子扣減或者數(shù)據(jù)庫加樂觀鎖。// server/controllers/coupon.js async function claimCoupon(req, res) { const { couponId } req.body; const userId req.userId; // 用 Redis 原子操作扣庫存key 是 coupon:stock:{id} const stockKey coupon:stock:${couponId}; const remain await redis.decr(stockKey); if (remain 0) { // 庫存不足回滾 await redis.incr(stockKey); return res.status(400).json({ msg: 券已領(lǐng)完 }); } // 檢查用戶是否已領(lǐng)過 const exist await UserCoupon.findOne({ where: { userId, couponId } }); if (exist) { await redis.incr(stockKey); // 回滾庫存 return res.status(400).json({ msg: 已領(lǐng)取過 }); } // 寫領(lǐng)取記錄 await UserCoupon.create({ userId, couponId, status: unused }); res.json({ msg: 領(lǐng)取成功 }); }邏輯說明先用decr原子扣減 Redis 庫存扣到負(fù)數(shù)說明沒了回滾并返回。然后查用戶是否重復(fù)領(lǐng)取重復(fù)也回滾。最后寫數(shù)據(jù)庫。參數(shù)上couponId從請(qǐng)求體拿userId從 token 解析。注意 Redis 庫存和數(shù)據(jù)庫庫存要同步常見做法是啟動(dòng)時(shí)把數(shù)據(jù)庫庫存刷進(jìn) Redis或者用定時(shí)任務(wù)對(duì)賬。3.3 訂單提交與返現(xiàn)審核用戶點(diǎn)完外賣后回到小程序提交訂單號(hào)和截圖進(jìn)入待審核狀態(tài)。// server/controllers/order.js async function submitOrder(req, res) { const { couponId, orderNo, amount, screenshot } req.body; const userId req.userId; // 校驗(yàn)參數(shù) if (!orderNo || !amount || amount 0) { return res.status(400).json({ msg: 參數(shù)不完整 }); } // 查券碼對(duì)應(yīng)的返現(xiàn)比例 const coupon await Coupon.findByPk(couponId); if (!coupon) return res.status(404).json({ msg: 券不存在 }); // 創(chuàng)建訂單狀態(tài) pending const order await Order.create({ userId, couponId, orderNo, amount, screenshot, status: pending, ratio: coupon.ratio // 返現(xiàn)比例比如 0.3 表示返 30% }); res.json({ orderId: order.id, msg: 提交成功等待審核 }); }后臺(tái)審核通過后觸發(fā)返現(xiàn)// server/controllers/admin.js async function approveOrder(req, res) { const { orderId } req.body; const order await Order.findByPk(orderId); if (!order || order.status ! pending) { return res.status(400).json({ msg: 訂單狀態(tài)異常 }); } // 計(jì)算返現(xiàn)金額 const cashbackAmount (order.amount * order.ratio).toFixed(2); // 事務(wù)更新訂單狀態(tài) 加余額 寫返現(xiàn)記錄 await sequelize.transaction(async (t) { await order.update({ status: approved }, { transaction: t }); await User.increment(balance, { by: cashbackAmount, where: { id: order.userId }, transaction: t }); await Cashback.create({ orderId: order.id, userId: order.userId, ratio: order.ratio, amount: cashbackAmount }, { transaction: t }); }); res.json({ msg: 審核通過已返現(xiàn) }); }這段代碼的核心是事務(wù)。訂單狀態(tài)、用戶余額、返現(xiàn)記錄三處必須一起成功或一起失敗否則會(huì)出現(xiàn)「錢返了但訂單還是待審核」這種玄學(xué)問題。ratio存在訂單表里而不是實(shí)時(shí)查券是為了防止運(yùn)營中途改比例導(dǎo)致歷史訂單返現(xiàn)金額對(duì)不上。3.4 管理后臺(tái)的訂單審核列表管理后臺(tái)一般用 Vue Element UI 或 React Ant Design。核心是一個(gè)帶篩選和分頁的表格加上審核按鈕。// admin/src/views/OrderList.vue邏輯部分 async function fetchOrders(params) { // params: { status, page, pageSize, keyword } const { data } await api.get(/admin/orders, { params }); orders.value data.list; total.value data.total; } async function handleApprove(orderId) { await api.post(/admin/order/approve, { orderId }); ElMessage.success(已返現(xiàn)); fetchOrders(currentParams); // 刷新列表 }后臺(tái)這塊沒什么黑科技關(guān)鍵是接口要支持按狀態(tài)、時(shí)間、訂單號(hào)篩選不然訂單一多運(yùn)營會(huì)瘋。常見做法是加一個(gè)「批量審核」接口但要注意批量操作里每單都要走事務(wù)別圖快寫成一條 SQL 更新所有。4. 本地跑起來環(huán)境準(zhǔn)備與啟動(dòng)步驟4.1 環(huán)境依賴與配置修改跑這套東西需要Node.js 14或 PHP 7.4看版本、MySQL 5.7、Redis可選但建議裝、微信開發(fā)者工具。先把數(shù)據(jù)庫建好導(dǎo)入源碼里的sql文件然后改config目錄下的配置。# config/default.js 示例 module.exports { db: { host: 127.0.0.1, port: 3306, user: root, password: your_password, // 改成你的 database: cashback_db // 改成你建的庫名 }, redis: { host: 127.0.0.1, port: 6379 }, wx: { appid: your_appid, // 微信小程序 AppID secret: your_secret // 微信小程序 Secret }, jwtSecret: change_this_to_random_string, cashback: { defaultRatio: 0.3 // 默認(rèn)返現(xiàn)比例 30% } };微信 AppID 和 Secret 在微信公眾平臺(tái)申請(qǐng)測(cè)試階段可以用測(cè)試號(hào)。jwtSecret一定要改別用默認(rèn)值不然 token 能被偽造。4.2 服務(wù)端與小程序端啟動(dòng)服務(wù)端啟動(dòng)cd server npm install npm run dev # 或 node app.js # 看到 Server running on port 3000 就成功了管理后臺(tái)啟動(dòng)cd admin npm install npm run serve # 默認(rèn)跑在 8080小程序端用微信開發(fā)者工具打開miniprogram目錄在app.js里把BASE_URL改成你本機(jī)的 IP注意不能用 localhost開發(fā)者工具要用局域網(wǎng) IP然后編譯預(yù)覽。// miniprogram/app.js const BASE_URL http://192.168.1.100:3000; // 改成你的局域網(wǎng) IP4.3 驗(yàn)證鏈路是否跑通按這個(gè)順序驗(yàn)證先在小程序里點(diǎn)登錄看后端有沒有生成用戶記錄然后領(lǐng)一張券看 Redis 庫存有沒有減接著提交一個(gè)訂單去管理后臺(tái)看列表里有沒有這條最后點(diǎn)審核通過回小程序看余額有沒有加。四步都通說明核心鏈路沒問題。提示如果登錄報(bào)「invalid code」多半是 AppID 或 Secret 配錯(cuò)了或者 code 被重復(fù)使用。檢查 config 里的微信配置并確認(rèn)前端沒有緩存 code。5. 避坑與常見問題排查5.1 券碼庫存扣成負(fù)數(shù)現(xiàn)象并發(fā)領(lǐng)券時(shí)Redis 庫存顯示 -3數(shù)據(jù)庫里領(lǐng)取記錄卻只有兩條。原因是decr之后沒有及時(shí)回滾或者回滾邏輯在異常分支里被跳過了。解決把扣庫存和回滾包在 try-catch 里確保任何異常都執(zhí)行incr。更穩(wěn)的做法是用 Lua 腳本把「檢查 扣減」做成原子操作。5.2 返現(xiàn)后余額對(duì)不上現(xiàn)象用戶反饋返了 10 塊但余額只加了 8 塊。原因通常是浮點(diǎn)數(shù)計(jì)算精度問題0.3 * 33.33在 JS 里不等于9.999。解決金額統(tǒng)一用「分」為單位存整數(shù)展示時(shí)再除以 100?;蛘哂胐ecimal.js這類庫處理。5.3 訂單截圖上傳失敗現(xiàn)象小程序端選完圖上傳后端收到的是空文件。原因是小程序wx.uploadFile的name字段和后端 multer 配置的字段名不一致。解決前端name: file后端multer({ dest: uploads/ }).single(file)兩邊對(duì)齊。另外注意上傳目錄要有寫權(quán)限。5.4 管理后臺(tái)跨域請(qǐng)求被攔現(xiàn)象后臺(tái)調(diào) API 報(bào) CORS 錯(cuò)誤。原因是 API 服務(wù)沒配跨域頭或者配了但沒處理 OPTIONS 預(yù)檢。解決在 Express 里加cors中間件app.use(cors({ origin: http://localhost:8080 }))。如果用了 Nginx 反代也可以在 Nginx 層加頭。5.5 微信支付回調(diào)收不到現(xiàn)象用戶付了錢但訂單狀態(tài)沒變。原因是回調(diào)地址沒配或者沒做驗(yàn)簽。解決確認(rèn)微信商戶平臺(tái)里的回調(diào) URL 是公網(wǎng)可訪問的回調(diào)處理里先驗(yàn)簽再改狀態(tài)且要返回微信要求的 XML 格式。本地開發(fā)可以用內(nèi)網(wǎng)穿透工具把本機(jī)暴露出去測(cè)試。6. 二次開發(fā)改返現(xiàn)規(guī)則與加自動(dòng)審核拿到源碼后最常改的兩處返現(xiàn)規(guī)則和審核流程。返現(xiàn)規(guī)則現(xiàn)在是在coupons表里存一個(gè)固定ratio如果你想做「首單返 50%后續(xù)返 20%」可以在submitOrder里根據(jù)用戶歷史訂單數(shù)動(dòng)態(tài)算比例。// 動(dòng)態(tài)返現(xiàn)比例示例 async function calcRatio(userId, coupon) { const orderCount await Order.count({ where: { userId, status: approved } }); if (orderCount 0) return 0.5; // 首單返 50% if (orderCount 3) return 0.3; // 前 3 單返 30% return 0.2; // 之后返 20% }自動(dòng)審核的思路是對(duì)接外賣平臺(tái)的訂單查詢接口如果有用訂單號(hào)去核驗(yàn)金額和狀態(tài)匹配上了就自動(dòng)通過。沒有接口的話可以做一個(gè)「截圖 OCR 金額比對(duì)」的降級(jí)方案用第三方 OCR 識(shí)別截圖里的訂單金額和用戶填的比對(duì)誤差在 1 元內(nèi)就自動(dòng)過。// 自動(dòng)審核偽代碼 async function autoAudit(order) { const ocrResult await ocr.recognize(order.screenshot); const ocrAmount extractAmount(ocrResult); // 從 OCR 文本里提金額 if (Math.abs(ocrAmount - order.amount) 1) { await approveOrder(order.id); // 自動(dòng)通過 } else { await order.update({ status: manual }); // 轉(zhuǎn)人工 } }改完記得跑一遍完整鏈路驗(yàn)證新用戶首單提交 → 自動(dòng)審核 → 余額增加 → 第二單比例變化。我一般會(huì)在server/test下寫幾個(gè)集成測(cè)試用 supertest 打接口避免每次改完手動(dòng)點(diǎn)。從那以后我每次改返現(xiàn)邏輯都強(qiáng)制走一遍「新用戶 老用戶 駁回重提」三條用例不然總有一端對(duì)不上。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取