本項目爲基於微信手機應用平臺的一款運動互動型小程序,實現了用戶即時運動步數羣內PK與我的動態的發佈,小程序前端採用原生框架,後端採用基於Node的koa2框架,數據庫採用MYSQL,對象存儲採用七牛雲,服務器採用阿里ECS,域名採用CA認證。javascript
運行效果以下
http://pgwn32i53.bkt.clouddn....html
在這個項目中,有個功能爲動態發佈,容許用戶上傳圖片,動態發佈後,全部人可在動態廣場看到該動態,存儲圖片有不少方式,例如經過表單將文件轉爲爲二級制發送給後端,而後存數據庫中,可是,這樣我就要多寫不少代碼,全部我決定將圖片存儲到圖牀,我數據庫中保存圖片連接便可,圖牀有不少,不一一描述了,我此次用的是七牛雲,我的認證成功後將得到必定空間的免費存儲空間。前端
創建存儲空間(ydonline),選定存儲區域(華北)。java
註冊成功後,將得到兩組祕鑰,這東西很重要,上傳文件時,須要知道uptoken,uptoken是根據AK與SC生成的,七牛雲開發文檔中建議後端生成uptoken值,但我嫌麻煩,直接在線生成了uptoken,也就是說該uptoken是寫死的。 node
uptoken生成地址:http://pchou.qiniudn.com/qini... deadline的時間設置長一些mysql
引入官方開發文件:qiniuUploader.jsios
小程序端存儲圖片關鍵代碼:git
releaseNotice.js const qiniuUploader = require("qiniuUploader.js"); function didPressChooesImage(that) { initQiniu(); // 微信 API 選文件 wx.chooseImage({ count: 1, success: function (res) { var filePath = res.tempFilePaths[0]; // 交給七牛上傳 qiniuUploader.upload(filePath, (res) => { let imageurl = that.data.imageurl; imageurl.push(getApp().data.qiniu_domain + res.key); that.setData({ 'imageurl': imageurl }); }, (error) => { console.error('error: ' + JSON.stringify(error)); }); } } ) } function initQiniu() { //配置詳解 https://github.com/gpake/qiniu-wxapp-sdk/blob/master/README.md var options = { region: 'NCN', // 華東區 根據存儲區域填寫 //uptokenURL: 'UpTokenURL.com/uptoken',//// 從指定 url 經過 HTTP GET 獲取 uptoken,返回的格式必須是 json 且包含 uptoken 字段,例如: {"uptoken": "0MLvWPnyy..."} uptoken: getApp().data.qiniu_uptoken, domain: getApp().data.qiniu_domain }; qiniuUploader.init(options); } --------------------------------------------------------------------------------------- app.js App({ data: { qiniu_domain: 'http://pgwn32i53.bkt.clouddn.com/',//七牛圖片外鏈域名 qiniu_uptoken: 'uxQXOgxXDtF-1uM_V15KQSIky5Xkdww0GhoAksLF:LWUt0HMVbICEDaSOMnMF3YLoUH4=:eyJzY29wZSI6Inlkb25saW5lIiwiZGVhZGxpbmUiOjE4NTU2NzA0MDF9' }, })
上傳文件到指定的存儲空間,七牛會返回文件的key值,加上七牛給你的外鏈域名,這樣,你就能夠顯示文件了。github
我買過兩次服務器,第一次是阿里的,另一次也是阿里的。但此次,我買的是windows版雲ECS ,首先,明確需求。算法
在windows 服務器上部署項目 簡單, 遠程連接時,選擇共享本地某個硬盤的資料,鏈接成功後,把本地的環境軟件所有安裝上去,本次須要在服務器上安裝 node.js、git、mysql這三個軟件。
購買SSL證書
https://yundunnext.console.al...
阿里有對單域名免費的證書,時間爲1年,因而我爲該項目的域名購買了https。
點擊下載 =>選擇 其餘
解壓後,裏面有兩個文件,一個是crt,一個是key,將這兩個文件發在後臺文件夾下/ssl包下 (可隨便命名)
後臺加載這兩個文件。
app.js 關鍵代碼 var app = require('koa'), https = require('https'); app=new app(); var options = { key: fs.readFileSync('./ssl/key.key'), //ssl文件路徑 cert: fs.readFileSync('./ssl/crt.crt') //ssl文件路徑 }; // start the server https.createServer(options, app.callback()).listen(443);
這樣,後臺就跑在htpps下了。
在服務器上運行後臺:
而後你用本身的電腦打開該域名,你會發現根本鏈接不上。
<!--more-->
那麼,這是什麼鬼?
原來 阿里的windows 服務器 防火牆作了限制,且服務器安全組也作了限制。
打開防火牆=>高級設置=>入站規則=>新建規則。
選擇端口 =>填寫端口
本次須要填寫三個端口號, 後臺的80(http) 443(https) 3306(mysql)
填寫 那三個端口號, 受權對象那裏 填寫 0.0.0.0/0
作完這一步,大功告成了,你的域名能夠被訪問了。
數據表有兩張,分爲動態表與運動數據表,以下圖所示。
首頁主要由動態廣場與底部的tabbar組成,動態廣場顯示狀態正常(state==1)的動態,私人動態與禁止動態不能被他人所看見,在數據表設計中,uid其實就是openid。 用戶上傳了圖片後,數據庫中保存的是其訪問地址,多個地址之間用逗號分隔,造成字符串, 前端拿到圖片地址後,將其轉成數組。
關鍵代碼以下:
onShow:async function(){ var that = this; let pageno = 1; let pagesize = this.data.pagesize; this.setData({ pageno:1}); noticesource.findalllnotice(pageno, pagesize).then(function (res) { console.log(res); for (let i in res) { let image = res[i].photo.split(','); res[i].imageurl = image; } that.setData({ allnotice: res }); }) },
動態頁面使用分頁加載,每次下拉加載10條,內容展現區使用 scroll-view組件,設置bindscrolltolower="upper",下拉時觸發upper方法。
關鍵代碼以下:
upper: function () { var allnotice = this.data.allnotice; var pageno = ++this.data.pageno; var pagesize = this.data.pagesize; console.log(pageno); var that = this; this.setData({ pageno: this.data.pageno++ }); noticesource.findalllnotice(pageno, pagesize).then(function (res) { console.log(res); for (let i in res) { let image = res[i].photo.split(','); res[i].imageurl = image; } if (res.length > 0) { allnotice=allnotice.concat(res); } console.log(allnotice); that.setData({ allnotice: Array.from(new Set(allnotice)) }); }) }
動態發佈頁主要有文本輸入框,圖片上傳區、發佈按鈕組成。
該頁面爲重要頁面,在發佈前,咱們須要獲取發佈者的頭像、暱稱、openid,若是這三個必要條件缺一的話,咱們就會阻止用戶發佈動態而且提示用戶受權登陸。
獲取openid關鍵代碼以下:
app.js wx.login({ success(res) { console.log('code: ' + res.code); if (res.code) { loginsource.login(res.code).then(function (data) { console.log(data); wx.setStorage({ key: 'openid', data: data.openid, }) wx.setStorage({ key: 'session_key', data: data.session_key, }) }) } else { console.log('登陸失敗!' + res.errMsg) } } });
用戶在進入小程序時,我就會讓一次後臺請求,根據wx.login返回的code解密生成 openid與 session_key,我將其存放在storage中,其實,我不推薦將session_key放在storage中,按正常開發方式,應該是後臺臨時存儲session_key,並返回一個sessionid給用戶,不該該把session_key返回給用戶。可是,我在用koa-session時,一直沒有成功,不知道什麼鬼!沒辦法,我只好把session_key返回給用戶了。
上傳圖片關鍵代碼:
didPressChooesImage: function () { if (this.data.imageurl.length>=4){ wx.showToast({ title:"不能超過4張", }) return } var that = this; didPressChooesImage(that); //沒有寫錯,只是同名,見2.1節 }, //刪除指定圖片 deleteImage: function (e) { let index = e.currentTarget.dataset.index; var that = this; var imagetemp = that.data.imageurl; imagetemp.splice(index, 1); that.setData({ imageurl: imagetemp }); },
個人頁面包含了用戶的數據統計信息與小程序的推廣信息,目前有待完善,從個人頁面進去的動態頁面只包含用戶本身發佈的。
在未登錄時,頁面顯示默認頭像,點擊頭像,受權我的信息,顯示微信頭像與暱稱。
接下來,重頭戲來了,運動數據pk爲該小程序的核心功能,
功能點:
1)獲取用戶此時的運動步數並展現出來
2)分享本身的運動步數到微信羣 並在頁面上造成 pk排名區
3)其餘用戶經過分享進入小程序,系統獲取其羣id與運動步數 與同一微信羣的用戶進行pk
4)每次分享或點擊分享連接,系統將自動更新該用戶的運動步數
5) pk排名區只展現當日的排名狀況,晚上12點後自動清空pk區
技術點:
1)獲取用戶運動步數
2)獲取羣id
3) 各羣之間間運動數據隔離
效果以下圖所示
受權後顯示步數。
點擊選擇一個聊天后,將發佈分享到微信羣裏,分享成功後,前端獲取到ShareTicket,羣內其餘人經過該連接進來,前端也會獲取到ShareTicket,調用 wx.getShareInfo()將加密數據發送給後端解密,可得到 openGid ,將用戶的步數與openGid等信息存儲起來,造成了groupsport表。
保存當日已分享的羣id,獲取ShareTicket 關鍵代碼:
onLoad: function (opt) { //在storage中建立用戶的當日分享狀況 也就是分享到了哪些羣,將這些羣id存在一個與日期掛鉤的對象中,到了次日,清空羣id. var that =this; var nowDate = new Date(); var year = nowDate.getFullYear(); var month = nowDate.getMonth() + 1 < 10 ? "0" + (nowDate.getMonth() + 1) : nowDate.getMonth() + 1; var day = nowDate.getDate() < 10 ? "0" + nowDate.getDate() : nowDate .getDate(); var dateStr = year + "-" + month + "-" + day; var value = wx.getStorageSync('openGidlist') if (value!=undefined||value!=null) { console.log(value); if (value.date != dateStr || value.Gidlist == undefined || value.Gidlist==null){ wx.setStorageSync('openGidlist', { date: dateStr, Gidlist: [] }); } } //設置開啓ShareTicket wx.showShareMenu({ withShareTicket: true }) }
獲取我的運動數據
onShow: function () { wx.getWeRunData({ success: function (res) { console.log(res); let session_key = wx.getStorageSync("session_key"); console.log(session_key); sportsource.getsportdata(res, session_key).then(function (data) { console.log(data); that.setData({ todaysportcount: data.data.stepInfoList[30].step }); }) } }) }
分享時獲取羣id
onShareAppMessage: function () { var nowDate = new Date(); var year = nowDate.getFullYear(); var month = nowDate.getMonth() + 1 < 10 ? "0" + (nowDate.getMonth() + 1) : nowDate.getMonth() + 1; var day = nowDate.getDate() < 10 ? "0" + nowDate.getDate() : nowDate .getDate(); var dateStr = year + "-" + month + "-" + day; var that=this; return { title: '我已經運動了' + this.data.todaysportcount+'步,你敢來PK嗎?', path: 'pages/sportpk/sportpk', success: function (res) { var shareTickets = res.shareTickets; if (shareTickets.length == 0) { return false; } console.log('shareTickets'+shareTickets[0]); wx.getShareInfo({ shareTicket: shareTickets[0], success: function (res) { var encryptedData = res.encryptedData; var iv = res.iv; console.log(res); let session_key = wx.getStorageSync("session_key"); sportsource.getsportdata(res, session_key).then(function (data) { console.log(data); let openGid = data.data.openGId; that.setData({ openGid: openGid}); let openid = that.data.openid; let todaysportcount = that.data.todaysportcount; let avatarUrl = that.data.userInfo.avatarUrl; let nickname = that.data.userInfo.nickName; if (todaysportcount == undefined || todaysportcount==null){ wx.showToast({ title: '請從新打開運動權限', duration: 2000 }) return } if (avatarUrl == undefined || avatarUrl == null || nickname == undefined || nickname==null) { wx.showToast({ title: '請先點擊頭像登陸', duration: 2000 }) return } // let openGidlist = that.data.openGidlist; let openGidlist = wx.getStorageSync('openGidlist'); openGidlist.Gidlist.push(openGid); openGidlist.Gidlist = [...new Set(openGidlist.Gidlist)] console.log("---------------------------------------"); console.log('openGidlist.Gidlist' + openGidlist.Gidlist); console.log("---------------------------------------"); wx.setStorageSync('openGidlist', { date: dateStr, Gidlist: openGidlist.Gidlist}); sportsource.creategroupsport({ openGid, openid, todaysportcount, avatarUrl, nickname }).then(function(){ that.setData({ sharegroupdata: [] }); that.getfirstgroupsport(); }) }) } }) }, fail: function (res) { // 轉發失敗 } } }
刷新羣內pk
getfirstgroupsport: async function () { var that = this; let openGidlist = wx.getStorageSync('openGidlist'); if (openGidlist.Gidlist != undefined || openGidlist.Gidlist != null) { if (openGidlist.Gidlist.length > 0) { for (let i in openGidlist.Gidlist) { let data = await sportsource.getgroupsport(openGidlist.Gidlist[i]); console.log(data); let sharegroupdata = that.data.sharegroupdata; sharegroupdata.push(data.data.rows); that.setData({ sharegroupdata: sharegroupdata }) } } } },
點擊分享連接獲取shareTicket,經過連接進入小程序的場景值是1044
app.js onShow (opt){ console.log("opt.scene" + opt.scene); if (opt.scene==1044){ this.globalData.shareTicket = opt.shareTicket; } },
後端採用的是koa2,經過sequelize.js實現與mysql的鏈接。
function getOpenId(code) { console.log(code); return new Promise((resolve, reject) => { const id = ''; // AppID(小程序ID) const secret = '';// AppSecret(小程序密鑰) let url = `https://api.weixin.qq.com/sns/jscode2session?appid=${id}&secret=${secret}&js_code=${code}&grant_type=authorization_code`; axios.get(url).then(function (response) { console.log("response.data:"+response.data); resolve(response.data); }) .catch(function (error) { console.log(error); reject(error); }); }) } async function login(ctx) { const {code} = ctx.query; const data = await getOpenId(code); ctx.body = data; }
這兩個數據屬於隱私數據,須要用算法解密,具體的流程,微信開發手冊上有,我就很少說了,須要用到上一步的session_key來解密。
//WXBizDataCryptconst:微信提供的解密方法 WXBizDataCryptconst WXBizDataCrypt = require('./WXBizDataCrypt') var appId = ''; async function getsportdata(ctx) { var encryptedData=ctx.query.encryptedData; var iv=ctx.query.iv; var session_key=ctx.query.session_key; console.log("session_key"+session_key); var pc = new WXBizDataCrypt(appId,session_key); var data = pc.decryptData(encryptedData,iv); console.log('解密後 data: ', data); ctx.body={ success:true, data:data } }
當用戶分享本身的運動數據到微信羣內時或者微信羣內其餘用戶經過該連接進入小程序時,後端將獲建立或者更新該用戶的羣內運動數據。
const creategroupsport = async function(data){ // 給某個用戶建立一條羣運動記錄 let nowDate = new Date(); let year = nowDate.getFullYear(); let month = nowDate.getMonth() + 1 < 10 ? "0" + (nowDate.getMonth() + 1) : nowDate.getMonth() + 1; let day = nowDate.getDate() < 10 ? "0" + nowDate.getDate() : nowDate .getDate(); let dateStr = year + "-" + month + "-" + day; var countdata = await Todolist.findAndCount({ where:{openGid:data.openGid,createdate:dateStr,openid: data.openid} }) var count=0; if(countdata!=undefined||countdata!=null) { count = countdata.count; } if(count==0) { await Todolist.create({ openGid: data.openGid, openid: data.openid, todaysportcount: data.todaysportcount, createdate: dateStr, avatarUrl: data.avatarUrl, nickname: data.nickname }) } else { await Todolist.update({ todaysportcount: data.todaysportcount, avatarUrl: data.avatarUrl, nickname: data.nickname, openid: data.openid, },{ where:{ id:countdata.rows[0].id } }) } return true }
//讀取羣內運動數據 const getgroupsport = async function(openGid){ console.log('openGid'+openGid); let nowDate = new Date(); let year = nowDate.getFullYear(); let month = nowDate.getMonth() + 1 < 10 ? "0" + (nowDate.getMonth() + 1) : nowDate.getMonth() + 1; let day = nowDate.getDate() < 10 ? "0" + nowDate.getDate() : nowDate .getDate(); let dateStr = year + "-" + month + "-" + day; const data= await Todolist.findAndCount({ where:{openGid:openGid,createdate:dateStr}, order: [ ['todaysportcount', 'DESC'] ] }) console.log(data); return data; }
對用戶發佈的動態,後臺目前主要有 增,改,查三類方法,我說一下分頁查詢。
const findallnotice = async function(ctx){ // 查詢全部 let pageno=ctx.pageno; let pagesize=ctx.pagesize; console.log(pageno,pagesize); const data= await Todolist.findAndCount({ where: {state:1}, offset:(pageno - 1) * pagesize,//開始的數據索引,好比當page=2 時offset=10 ,而pagesize咱們定義爲10,則如今爲索引爲10,也就是從第11條開始返回數據條目 limit:pagesize*1//每頁限制返回的數據條數 }) console.log(data); return data; } const findmynotice = async function(ctx){ // 查詢本身的 let pageno=ctx.pageno; let pagesize=ctx.pagesize; let uid=ctx.openid; console.log(pageno,pagesize,uid); const data= await Todolist.findAndCount({ where:{uid:uid}, offset:(pageno - 1) * pagesize,//開始的數據索引,好比當page=2 時offset=10 ,而pagesize咱們定義爲10,則如今爲索引爲10,也就是從第11條開始返回數據條目 limit:pagesize*1//每頁限制返回的數據條數 }) console.log(data); return data; }
我洋洋灑灑寫了幾千字,看上去舉重若輕,可是在實際開發中常常會遇見各類各樣的問題,該項目從原型設計與開發到部署都是我獨自完成的,中間也踩了一些坑,這個項目最終沒能上線,是由於,我的主體帳號不能發佈關於GUC的小程序。
本文首發於個人我的博客: https://www.catac.cn,轉載時請註明來源,
該項目源碼地址:https://github.com/ouminglian...
也歡迎各位與我交流,我的QQ:2541511219