器也能做外賣紅包CPS小程序?云開發(fā)模式全拆解)
簡(jiǎn)介這是一套面向個(gè)人開發(fā)者、主打零服務(wù)器成本的外賣紅包CPS小程序源碼重點(diǎn)匹配微信小程序個(gè)人主體過審條件讓沒有技術(shù)團(tuán)隊(duì)和獨(dú)立后端預(yù)算的個(gè)人也能搭建屬于自己的返利推廣工具。壓縮包約3.51MB整體非常輕量便于直接導(dǎo)入微信開發(fā)者工具查看頁(yè)面結(jié)構(gòu)與邏輯代碼中應(yīng)包含小程序前后臺(tái)頁(yè)面、紅包發(fā)放流程、CPS訂單跟進(jìn)參數(shù)以及流量主廣告位的接入邏輯。已有637人學(xué)習(xí)使用說明它在同類資源中具備一定的參考價(jià)值。除了跑通外賣紅包發(fā)放與傭金跟蹤的核心鏈路外資源還融合了菜譜內(nèi)容和流量主變現(xiàn)機(jī)制相當(dāng)于提供一個(gè)可長(zhǎng)期運(yùn)營(yíng)的入口開發(fā)者可在源碼基礎(chǔ)上自由調(diào)整界面風(fēng)格、對(duì)接不同外賣平臺(tái)API并設(shè)計(jì)更多促活玩法降低從開發(fā)到上線的試錯(cuò)成本。1. 為什么說“無需服務(wù)器”云開發(fā)模式的取舍邊界拿到這套外賣紅包 CPS 小程序源碼第一眼最吸引人的就是「無需服務(wù)器」四個(gè)字。做過微信生態(tài)的人都知道傳統(tǒng)小程序后端要買云服務(wù)器、裝環(huán)境、配域名、過備案審核來回改一個(gè)新手光把 HTTPS 證書掛對(duì)就得折騰一兩天。這套源碼把后端邏輯全部放到了小程序云開發(fā)上云函數(shù)處理接口、云數(shù)據(jù)庫(kù)存菜品和紅包配置、云存儲(chǔ)放圖片開發(fā)者在微信開發(fā)者工具里點(diǎn)一下「上傳并部署」剩下的托管、負(fù)載、安全都由平臺(tái)扛了。對(duì)日活幾十到幾百人的外賣導(dǎo)購(gòu)場(chǎng)景云開發(fā)免費(fèi)額度已經(jīng)夠用等跑起來有收益再升級(jí)配額屬于典型的小步快跑玩法。但免費(fèi)額度背后也有冷啟動(dòng)、并發(fā)上限、超時(shí)時(shí)間這些硬約束本文先把鏈路講透再拆源碼和流量主接入最后你拿到源碼改起來就知道哪里能碰、哪里要繞道。2. 外賣紅包 CPS 鏈路拆解從推廣位到下單錢到底怎么算CPS 是 Cost Per Sale按成交付費(fèi)和 CPS 最常見的區(qū)別是用戶必須完成有效訂單推廣方才能拿到傭金。外賣紅包 CPS 的邏輯就是用戶在你的小程序里領(lǐng)紅包領(lǐng)完跳轉(zhuǎn)到外賣平臺(tái) H5 或小程序下單訂單核銷后聯(lián)盟后臺(tái)給你結(jié)算傭金。這套源碼把整個(gè)鏈路做成了「微信小程序 - 云函數(shù) - 聯(lián)盟 API - 外賣平臺(tái)」先理解鏈路里每一個(gè)環(huán)節(jié)才知道源碼里的接口為什么這么寫。2.1 聯(lián)盟平臺(tái)選型與推廣位 PID紅包 CPS 的操作入口通常是美團(tuán)外賣聯(lián)盟、餓了么的「阿里媽媽/淘寶聯(lián)盟」或第三方聚合平臺(tái)。個(gè)人開發(fā)者注冊(cè)聯(lián)盟賬號(hào)后需要?jiǎng)?chuàng)建一個(gè)媒體位或推廣位拿到一串推廣位 ID比如美團(tuán)的pid通常是類似10xxxxx的數(shù)字串淘寶聯(lián)盟的則是mm_xxx_xxx_xxx三段式。這個(gè) ID 是 CPS 結(jié)算的唯一歸屬憑證用戶必須通過拼了該 ID 的鏈接領(lǐng)紅包訂單才會(huì)記到你的名下。源碼里一般不會(huì)把 PID 寫死在代碼中而是放到云開發(fā)數(shù)據(jù)庫(kù)的一個(gè)配置集合中方便隨時(shí)換。我一般會(huì)在config集合里存一條記錄字段包括meituanPid、elemePid、appKey、secret。這樣做的好處是聯(lián)盟后臺(tái)某個(gè)推廣位被平臺(tái)風(fēng)控了你只需要在數(shù)據(jù)庫(kù)改不用改代碼重新發(fā)布。2.2 云函數(shù)做鏈接拼接與簽名外賣平臺(tái)的 CPS 開放接口通常要求請(qǐng)求簽名常見做法是把pid、scene、timestamp等參數(shù)按字典序排列用 secret 做 HMAC-SHA1 或 MD5 生成簽名。這套源碼里核心的一個(gè)云函數(shù)就是getCpsLink入?yún)⑹莵碓磮?chǎng)景返回值是拼接后的跳轉(zhuǎn)鏈接。// cloudfunctions/getCpsLink/index.js const cloud require(wx-server-sdk) cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main async (event, context) { const { scene default } event const wxContext cloud.getWXContext() const openid wxContext.OPENID const db cloud.database() // 從數(shù)據(jù)庫(kù)讀取聯(lián)盟配置 const configRes await db.collection(config).doc(cps).get() const { pid, secret, baseUrl } configRes.data const timestamp Date.now() // 平臺(tái)簽名規(guī)則參數(shù)按 key 排序后拼接 secret const signStr pid${pid}scene${scene}timestamp${timestamp}${secret} const crypto require(crypto) const sign crypto.createHash(md5).update(signStr).digest(hex) const cpsUrl ${baseUrl}?pid${pid}scene${scene}timestamp${timestamp}sign${sign} // 寫入領(lǐng)取記錄方便做防刷 await db.collection(cps_log).add({ data: { openid, scene, url: cpsUrl, createTime: Date.now() } }) return { cpsUrl } }這段代碼有幾個(gè)值得留意的點(diǎn)cloud.DYNAMIC_CURRENT_ENV表示云函數(shù)運(yùn)行時(shí)自動(dòng)使用當(dāng)前云環(huán)境不需要寫死環(huán)境 IDscene來自前端傳入用來標(biāo)明用戶是從首頁(yè) Banner、菜譜頁(yè)還是個(gè)人中心進(jìn)入的便于后續(xù)做渠道統(tǒng)計(jì)日志集合cps_log不僅是存記錄更是防刷判斷的數(shù)據(jù)來源。實(shí)際對(duì)接時(shí)聯(lián)盟平臺(tái)的簽名算法可能在參數(shù)名上有差異你只需替換signStr的拼裝順序即可整體結(jié)構(gòu)不需要?jiǎng)印?.3 前端跳轉(zhuǎn)復(fù)制鏈接 vs web-view拿到cpsUrl后小程序里怎么跳轉(zhuǎn)是個(gè)必須提前想清楚的問題。平臺(tái)限制個(gè)人主體小程序的web-view組件只能打開業(yè)務(wù)域名內(nèi)的網(wǎng)頁(yè)而外賣聯(lián)盟的 CPS 鏈接域名并不在你配置的合法域名列表中直接放web-view大概率白屏。源碼里常見做法是給用戶兩個(gè)出口一個(gè)是復(fù)制鏈接讓用戶粘貼到微信聊天中自動(dòng)識(shí)別另一個(gè)是生成小程序碼用戶長(zhǎng)按掃碼后打開 H5 頁(yè)面。view classcoupon-card wx:for{{channels}} wx:keyname text classname{{item.name}}/text text classdesc最高立減 {{item.discount}} 元/text button sizemini bindtapcopyLink>copyLink(e) { const url e.currentTarget.dataset.url || wx.setClipboardData({ data: url, success: () { wx.showToast({ title: 鏈接已復(fù)制去微信粘貼打開, icon: none }) } }) }復(fù)制鏈接這個(gè)交互不那么流暢但勝在穩(wěn)定、不依賴額外配置。如果你想提升轉(zhuǎn)化可以做一層「在微信中打開」的中間頁(yè)用戶復(fù)制后彈窗提示去微信搜索框粘貼讓微信內(nèi)部直接喚起外賣小程序。別小看這一步?jīng)Q定用戶是關(guān)掉小程序還是去領(lǐng)紅包。2.4 需要理解的幾個(gè)參數(shù)參數(shù)含義是否參與簽名常見坑pid推廣位 ID傭金歸屬是填錯(cuò)或填成別人的白給別人導(dǎo)量sid子渠道標(biāo)識(shí)是視平臺(tái)用于區(qū)分不同頁(yè)面來源不傳則渠道數(shù)據(jù)缺失timestamp請(qǐng)求時(shí)間戳是與服務(wù)器時(shí)間差超過 5 分鐘會(huì)被拒絕sign簽名串否由其他參數(shù)生成注意參數(shù)順序按平臺(tái)文檔來scene業(yè)務(wù)場(chǎng)景標(biāo)記否建議用home_banner這類語義化命名上面的表是大多數(shù) CPS 平臺(tái)的通用參數(shù)具體以你對(duì)接的聯(lián)盟文檔為準(zhǔn)。源碼中常出現(xiàn)sid和scene混用因?yàn)樗鼈兌汲袚?dān)渠道標(biāo)記職責(zé)有的平臺(tái)叫法不同本質(zhì)一樣。3. 源碼目錄與核心頁(yè)面紅包頁(yè)和菜譜頁(yè)是怎么組織的拿到源碼后先別急著改把目錄結(jié)構(gòu)掃一遍就能看出這套程序的骨架。它典型的小程序結(jié)構(gòu)分為miniprogram前端目錄和cloudfunctions云函數(shù)目錄前端頁(yè)面集中在pages下互相獨(dú)立改一個(gè)不影響另一個(gè)。3.1 項(xiàng)目目錄結(jié)構(gòu)project/ ├── project.config.json ├── cloudfunctions/ │ ├── getCpsLink/ # 生成 CPS 領(lǐng)券鏈接 │ ├── getRecipeList/ # 獲取菜譜列表 │ └── login/ # 登錄拿到 openid └── miniprogram/ ├── app.js ├── app.json ├── app.wxss ├── images/ ├── data/ │ └── recipes.json # 本地菜譜數(shù)據(jù)兜底 └── pages/ ├── index/ # 外賣紅包主頁(yè)面 ├── recipes/ # 菜譜列表頁(yè) ├── recipeDetail/ # 菜譜詳情頁(yè) └── mine/ # 個(gè)人中心recipes.json作為本地?cái)?shù)據(jù)兜底很有意思。很多類似源碼會(huì)把菜譜數(shù)據(jù)直接寫成前端 JSON 文件不去請(qǐng)求云數(shù)據(jù)庫(kù)這樣即使云函數(shù)調(diào)用失敗頁(yè)面也不會(huì)白屏。對(duì)于外賣場(chǎng)景菜譜不是核心業(yè)務(wù)即使數(shù)據(jù)是靜態(tài)的影響也不大。如果后期想豐富內(nèi)容再遷移到云數(shù)據(jù)庫(kù)也不遲。3.2 紅包主頁(yè)面卡片渲染與參數(shù)傳遞首頁(yè)是所有流量的入口決定了 CPS 的點(diǎn)擊率。這套源碼的首頁(yè)一般用wx:for渲染多個(gè)紅包卡片每個(gè)卡片綁定不同的scene參數(shù)點(diǎn)擊時(shí)先調(diào)云函數(shù)拿鏈接再?gòu)棾鰪?fù)制彈窗。view classbanner image src{{bannerUrl}} modeaspectFill/image button classearn-btn bindtaponEarn每天領(lǐng)外賣紅包/button /view view classcoupon-list view classcoupon-item wx:for{{coupons}} wx:keyid view classinfo view classname{{item.title}}/view view classsub{{item.subTitle}}/view /view button classget-btn bindtaponGetCoupon>onGetCoupon(e) { const scene e.currentTarget.dataset.scene wx.showLoading({ title: 生成中 }) wx.cloud.callFunction({ name: getCpsLink, data: { scene } }).then(res { wx.hideLoading() const cpsUrl res.result.cpsUrl wx.setClipboardData({ data: cpsUrl }) }).catch(() { wx.hideLoading() wx.showToast({ title: 生成失敗稍后再試, icon: none }) }) }這里的scene參數(shù)很關(guān)鍵一定要讓前端顯式傳入而不是后端自己猜。有些新手改源碼時(shí)圖省事把scene寫死在云函數(shù)里結(jié)果所有渠道數(shù)據(jù)都統(tǒng)計(jì)成一類后面調(diào)優(yōu)完全沒依據(jù)。3.3 菜譜頁(yè)列表篩選與本地?cái)?shù)據(jù)菜譜頁(yè)的功能定位很清晰用來拉長(zhǎng)用戶停留時(shí)間間接提高紅包點(diǎn)擊概率。源碼里通常有分類 Tab比如「快手菜」「湯羹」「主食」等切換時(shí)對(duì)本地 JSON 做過濾然后渲染列表。view classcategory-tabs view classtab {{activeCat item.id ? active : }} wx:for{{categories}} wx:keyid bindtapswitchCat>switchCat(e) { const id e.currentTarget.dataset.id this.setData({ activeCat: id }) this.filterRecipes() }, filterRecipes() { const all this.data.allRecipes if (this.data.activeCat all) { this.setData({ filteredRecipes: all }) return } const filtered all.filter(item item.cat this.data.activeCat) this.setData({ filteredRecipes: filtered }) }這個(gè)實(shí)現(xiàn)的特點(diǎn)是純前端操作沒有請(qǐng)求數(shù)據(jù)庫(kù)因此頁(yè)面秒開。如果你要擴(kuò)展成從云數(shù)據(jù)庫(kù)讀取需要注意小程序端where查詢一次最多返回 20 條需要做分頁(yè)否則超過 20 條后列表會(huì)靜默丟失一部分?jǐn)?shù)據(jù)。4. 流量主接入與個(gè)人主體過審廣告組件怎么放才不狼狽有了用戶量接下來就是流量主變現(xiàn)。這套源碼標(biāo)了「流量主版」說明廣告組件已經(jīng)接好了但真正把流量主開通、把廣告位塞進(jìn)頁(yè)面、通過審核中間有些細(xì)節(jié)源碼不會(huì)主動(dòng)提醒你。4.1 開通流量主的硬性門檻流量主的開通條件是「小程序累計(jì)獨(dú)立訪客不低于 1000」并且無嚴(yán)重違規(guī)記錄。這里的累計(jì)獨(dú)立訪客不等于日活而是從小程序上線開始算起的 UV。個(gè)人主體也可以開通但前提是你的小程序已經(jīng)通過微信認(rèn)證個(gè)人主體沒有認(rèn)證也可開通流量主實(shí)際上個(gè)人主體小程序也可以開通流量主但需要滿足 UV 條件。開通路徑登錄微信公眾平臺(tái)在左側(cè)菜單找「推廣」-「流量主」如果看到「立即開通」按鈕說明規(guī)模已達(dá)標(biāo)如果按鈕是置灰的說明 UV 還沒湊夠。很多開發(fā)者卡在這個(gè)門檻上可以讓親戚朋友幫忙搜索小程序或者在做地推時(shí)引導(dǎo)用戶掃小程序碼先把第一波 1000 UV 湊齊。4.2 Banner 廣告組件代碼里的正確姿勢(shì)流量主開通后在后臺(tái)「廣告管理」中創(chuàng)建廣告位會(huì)得到一串a(chǎn)dunit-xxxx的 ID。接下來要把廣告塞進(jìn)頁(yè)面。源碼里常見的是在菜譜列表和紅包頁(yè)底部放 Banner 廣告但很多新手會(huì)踩同一個(gè)坑把廣告組件直接放在頁(yè)面最底部結(jié)果展示率極低。// pages/index/index.json { usingComponents: {}, adUnitId: adunit-your-banner-id }view classad-container ad unit-id{{adUnitId}} ad-typebanner binderroronAdError/ad /view注意ad組件需要足夠?qū)挼娜萜鱽盹@示且不能嵌套在scroll-view里否則可能出現(xiàn)渲染異常。binderror事件一定要寫因?yàn)閺V告拉取失敗時(shí)小程序端會(huì)默認(rèn)加載不了導(dǎo)致頁(yè)面底部出現(xiàn)大塊空白加上事件后可以手動(dòng)隱藏這個(gè)容器。對(duì)于插屏廣告可以在用戶完成一次領(lǐng)紅包動(dòng)作后展示收益比 Banner 高但會(huì)打斷體驗(yàn)使用頻率要克制。激勵(lì)視頻廣告通常放在菜譜詳情頁(yè)讓用戶看視頻換取「隱藏菜譜」或「紅包升級(jí)」這類廣告對(duì)保留率和客單價(jià)都有明顯幫助。4.3 個(gè)人主體過審的文案邊界提到個(gè)人可以過審這套源碼顯然是按個(gè)人主體路徑打磨過的。個(gè)人主體的小程序在服務(wù)類目上受限外賣紅包這種業(yè)務(wù)容易涉及「商業(yè)服務(wù)」或「電商平臺(tái)」類目個(gè)人主體往往沒有對(duì)應(yīng)資質(zhì)。常見的解法是把小程序定位成「菜譜工具」「外賣紅包」只作為一個(gè)功能模塊而不是核心功能描述。這樣類目可以選擇「生活服務(wù) 美食菜譜」或「工具 效率」個(gè)人主體就能過。審核時(shí)注意事項(xiàng)還包括小程序標(biāo)題和簡(jiǎn)介中不要出現(xiàn)「返利」「賺傭金」「拉人頭」等敏感詞文案改為「平臺(tái)優(yōu)惠福利」。紅包金額的描述不要用「最高 100 元」這種帶極限詞的表述改成「隨機(jī)立減」。廣告組件必須在審核時(shí)能看到真實(shí)占位不能在無廣告 ID 時(shí)隱藏整個(gè)模塊否則審核人員可能認(rèn)為頁(yè)面缺失功能。用戶協(xié)議和隱私政策彈窗必須有即使不涉及真實(shí)交易也要在首次打開時(shí)以彈窗形式告知收集了用戶 openid。如果你的類目審核被拒優(yōu)先檢查頁(yè)面里有沒有「立即提現(xiàn)」「?jìng)蚪稹沟茸盅酆芏囝愃圃创a默認(rèn)文案是從網(wǎng)賺項(xiàng)目復(fù)制過來的不改成合規(guī)描述基本過不了。5. 進(jìn)階渠道場(chǎng)景追蹤與防刷校驗(yàn)源碼能跑通只是第一步把收益做上去需要在細(xì)節(jié)上下功夫。這里分享一個(gè)最實(shí)用的組合技巧用場(chǎng)景參數(shù)做渠道漏斗用云函數(shù)計(jì)數(shù)器做領(lǐng)紅包防刷。這兩個(gè)改動(dòng)不需要改頁(yè)面結(jié)構(gòu)只加一個(gè)云函數(shù)和少量數(shù)據(jù)庫(kù)邏輯但對(duì)長(zhǎng)期效果影響很大。場(chǎng)景參數(shù)scene不只是在 CPS 鏈接里透?jìng)饕淮嗡€會(huì)反映到聯(lián)盟后臺(tái)的報(bào)表中。部分聯(lián)盟平臺(tái)支持把sid作為二級(jí)渠道標(biāo)記你在小程序里給每個(gè)入口分配固定sid首頁(yè) Banner 用sidhome_top菜譜頁(yè)中部用sidrecipe_mid文章分享卡片用sidshare_card。跑兩周后從聯(lián)盟后臺(tái)的訂單列表看每個(gè)來源的轉(zhuǎn)化率如果某個(gè)入口曝光很高但零轉(zhuǎn)化就要檢查這個(gè)入口的文案或按鈕位置是不是有問題。這個(gè)過程不需要額外開發(fā)只是把scene值固化下來就行關(guān)鍵是別偷懶每次從后端獲取鏈接都要帶上特定值。防刷邏輯放在getCpsLink云函數(shù)里。外賣紅包 CPS 的常見攻擊方式是一個(gè) openid 在幾秒內(nèi)連續(xù)請(qǐng)求上百次把鏈接拿到后去發(fā)群但訂單是否屬于你這個(gè)推廣位以最后一次點(diǎn)擊歸因?yàn)闇?zhǔn)這種刷量既不會(huì)帶來傭金還會(huì)拉低小程序在聯(lián)盟側(cè)的風(fēng)控評(píng)級(jí)。常見的做法是同一用戶每天限制領(lǐng)取 3 次超過就拒絕生成鏈接。實(shí)現(xiàn)方式是在cps_log集合中查詢今天的記錄數(shù)如果超過閾值則直接短接。const MAX_DAILY 3 const startOfDay new Date() startOfDay.setHours(0, 0, 0, 0) const todayCount await db.collection(cps_log) .where({ openid, createTime: _.gte(startOfDay.getTime()) }) .count() if (todayCount.total MAX_DAILY) { return { code: -1, msg: 今日領(lǐng)取次數(shù)已達(dá)上限 } }這段邏輯注意幾個(gè)細(xì)節(jié)new Date()的時(shí)區(qū)應(yīng)以服務(wù)器時(shí)區(qū)為準(zhǔn)但微信云函數(shù)跑在 UTC 環(huán)境直接用setHours(0,0,0,0)得到的是 UTC 零點(diǎn)不是中國(guó)時(shí)區(qū)的零點(diǎn)。穩(wěn)妥的辦法是取當(dāng)前時(shí)間戳后減去new Date().getTimezoneOffset() * 60000再計(jì)算或者直接用云開發(fā)數(shù)據(jù)庫(kù)的db.serverDate()配合聚合函數(shù)統(tǒng)計(jì)當(dāng)天記錄。另外where中createTime字段建立索引否則數(shù)據(jù)量大了以后查詢會(huì)特別慢。云開發(fā)控制臺(tái)里你還能給cps_log集合加上免費(fèi)額度內(nèi)的容量預(yù)警超過 90% 時(shí)發(fā)短信通知避免日志占滿導(dǎo)致寫入失敗。總的原則是這套「云函數(shù) 云數(shù)據(jù)庫(kù)」方案雖然省了服務(wù)器但也要把免費(fèi)額度當(dāng)成資源來管理每次調(diào)用都做輕量化處理不打印無意義日志不在循環(huán)里查庫(kù)。把這些邊界看住外賣紅包 CPS 小程序才能走長(zhǎng)久。本文還有配套的精品資源點(diǎn)擊獲取