├── README.md ├── client ├── app.js ├── app.json ├── app.wxss ├── config.js ├── images │ ├── bg.png │ ├── check-yellow.png │ ├── check.png │ ├── discount.png │ ├── grey-arrow.png │ ├── home-sel.png │ ├── home.png │ ├── image.png │ ├── line-black.png │ ├── line-red.png │ ├── order-sel.png │ ├── order.png │ ├── trolley-sel.png │ ├── trolley.png │ ├── user-sel.png │ └── user.png ├── pages │ ├── add-comment │ │ ├── add-comment.js │ │ ├── add-comment.json │ │ ├── add-comment.wxml │ │ └── add-comment.wxss │ ├── comment │ │ ├── comment.js │ │ ├── comment.json │ │ ├── comment.wxml │ │ └── comment.wxss │ ├── detail │ │ ├── detail.js │ │ ├── detail.json │ │ ├── detail.wxml │ │ └── detail.wxss │ ├── home │ │ ├── home.js │ │ ├── home.json │ │ ├── home.wxml │ │ └── home.wxss │ ├── order │ │ ├── order.js │ │ ├── order.json │ │ ├── order.wxml │ │ └── order.wxss │ ├── trolley │ │ ├── trolley.js │ │ ├── trolley.json │ │ ├── trolley.wxml │ │ └── trolley.wxss │ └── user │ │ ├── user.js │ │ ├── user.json │ │ ├── user.wxml │ │ └── user.wxss ├── utils │ └── util.js └── vendor │ └── wafer2-client-sdk │ ├── LICENSE │ ├── README.md │ ├── index.js │ ├── lib │ ├── constants.js │ ├── login.js │ ├── request.js │ ├── session.js │ ├── tunnel.js │ ├── utils.js │ └── wxTunnel.js │ └── package.json ├── images&sql ├── demo.sql └── products │ ├── product1.jpg │ ├── product10.jpg │ ├── product11.jpg │ ├── product12.jpg │ ├── product13.jpg │ ├── product14.jpg │ ├── product15.jpg │ ├── product2.jpg │ ├── product3.jpg │ ├── product4.jpg │ ├── product5.jpg │ ├── product6.jpg │ ├── product7.jpg │ ├── product8.jpg │ └── product9.jpg ├── project.config.json └── server ├── README.md ├── app.js ├── config.js ├── controllers ├── .DS_Store ├── comment.js ├── index.js ├── login.js ├── message.js ├── order.js ├── product.js ├── trolley.js ├── tunnel.js ├── upload.js └── user.js ├── middlewares └── response.js ├── nodemon.json ├── package.json ├── process.prod.json ├── qcloud.js ├── routes └── index.js ├── tools.md ├── tools ├── cAuth.sql └── initdb.js └── utils └── db.js /README.md: -------------------------------------------------------------------------------- 1 | ## 优达学城 2 | 3 | ### 微信小程序开发(二)课程代码 4 | 5 | 6 | # Archival Note 7 | This repository is deprecated; therefore, we are going to archive it. However, learners will be able to fork it to their personal Github account but cannot submit PRs to this repository. If you have any issues or suggestions to make, feel free to: 8 | - Utilize the https://knowledge.udacity.com/ forum to seek help on content-specific issues. 9 | - Submit a support ticket along with the link to your forked repository if (learners are) blocked for other reasons. Here are the links for the [retail consumers](https://udacity.zendesk.com/hc/en-us/requests/new) and [enterprise learners](https://udacityenterprise.zendesk.com/hc/en-us/requests/new?ticket_form_id=360000279131). -------------------------------------------------------------------------------- /client/app.js: -------------------------------------------------------------------------------- 1 | //app.js 2 | var qcloud = require('./vendor/wafer2-client-sdk/index') 3 | var config = require('./config') 4 | 5 | let userInfo 6 | 7 | App({ 8 | onLaunch: function () { 9 | qcloud.setLoginUrl(config.service.loginUrl) 10 | }, 11 | 12 | login({ success, error }) { 13 | wx.getSetting({ 14 | success: res => { 15 | if (res.authSetting['scope.userInfo'] === false) { 16 | // 已拒绝授权 17 | wx.showModal({ 18 | title: '提示', 19 | content: '请授权我们获取您的用户信息', 20 | showCancel: false, 21 | success: () => { 22 | wx.openSetting({ 23 | success: res => { 24 | if (res.authSetting['scope.userInfo'] === true) { 25 | this.doQcloudLogin({ success, error }) 26 | } 27 | } 28 | }) 29 | } 30 | }) 31 | } else { 32 | this.doQcloudLogin({ success, error }) 33 | } 34 | } 35 | }) 36 | }, 37 | 38 | doQcloudLogin({success, error}) { 39 | // 调用 qcloud 登陆接口 40 | qcloud.login({ 41 | success: result => { 42 | if (result) { 43 | userInfo = result 44 | 45 | success && success({ 46 | userInfo 47 | }) 48 | } else { 49 | // 如果不是首次登录,不会返回用户信息,请求用户信息接口获取 50 | this.getUserInfo({ success, error }) 51 | } 52 | }, 53 | fail: () => { 54 | error && error() 55 | } 56 | }) 57 | }, 58 | 59 | getUserInfo({ success, error }){ 60 | if (userInfo) return userInfo 61 | 62 | qcloud.request({ 63 | url: config.service.user, 64 | login: true, 65 | success: result => { 66 | let data = result.data 67 | 68 | if (!data.code){ 69 | userInfo = data.data 70 | 71 | success && success({ 72 | userInfo 73 | }) 74 | } else { 75 | error && error() 76 | } 77 | }, 78 | fail: () => { 79 | error && error() 80 | } 81 | }) 82 | }, 83 | 84 | checkSession({ success, error }) { 85 | if (userInfo) { 86 | return success && success({ 87 | userInfo 88 | }) 89 | } 90 | 91 | wx.checkSession({ 92 | success: () => { 93 | this.getUserInfo({ 94 | success: res => { 95 | userInfo = res.userInfo 96 | 97 | success && success({ 98 | userInfo 99 | }) 100 | }, 101 | fail: () => { 102 | error && error() 103 | } 104 | }) 105 | }, 106 | fail: () => { 107 | error && error() 108 | } 109 | }) 110 | } 111 | 112 | }) -------------------------------------------------------------------------------- /client/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages":[ 3 | "pages/home/home", 4 | "pages/detail/detail", 5 | "pages/order/order", 6 | "pages/trolley/trolley", 7 | "pages/user/user", 8 | "pages/add-comment/add-comment", 9 | "pages/comment/comment" 10 | ], 11 | "window":{ 12 | "backgroundColor":"#F6F6F6", 13 | "backgroundTextStyle":"light", 14 | "navigationBarBackgroundColor": "#ececee", 15 | "navigationBarTitleText": "Wafer Quick Start", 16 | "navigationBarTextStyle":"black" 17 | }, 18 | "tabBar": { 19 | "list": [{ 20 | "pagePath": "pages/home/home", 21 | "text": "首页", 22 | "iconPath": "images/home.png", 23 | "selectedIconPath": "images/home-sel.png" 24 | }, 25 | { 26 | "text": "订单", 27 | "pagePath": "pages/order/order", 28 | "iconPath": "images/order.png", 29 | "selectedIconPath": "images/order-sel.png" 30 | }, 31 | { 32 | "text": "购物车", 33 | "pagePath": "pages/trolley/trolley", 34 | "iconPath": "images/trolley.png", 35 | "selectedIconPath": "images/trolley-sel.png" 36 | }, 37 | { 38 | "text": "个人中心", 39 | "pagePath": "pages/user/user", 40 | "iconPath": "images/user.png", 41 | "selectedIconPath": "images/user-sel.png" 42 | } 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /client/app.wxss: -------------------------------------------------------------------------------- 1 | /**app.wxss**/ 2 | page { 3 | background: #f9f9f9; 4 | } 5 | 6 | .bg { 7 | position: absolute; 8 | left: 0; 9 | top: 0; 10 | width: 100%; 11 | height: 144rpx; 12 | z-index: -1; 13 | } 14 | 15 | .unlogin-card { 16 | display: flex; 17 | align-items: center; 18 | margin: 50rpx 27rpx 0; 19 | height: 200rpx; 20 | background: #FFFFFF; 21 | box-shadow: 0 2rpx 13rpx 5rpx rgba(0, 0, 0, 0.02); 22 | border-radius: 13rpx; 23 | } 24 | 25 | .unlogin-head { 26 | flex-shrink: 0; 27 | margin-left: 53rpx; 28 | height: 100rpx; 29 | width: 100rpx; 30 | background: #F5E069; 31 | border-radius: 50%; 32 | } 33 | 34 | .unlogin-info { 35 | flex: 1; 36 | margin-left: 31rpx; 37 | } 38 | 39 | .unlogin-text { 40 | line-height: 48rpx; 41 | font-size: 34rpx; 42 | color: rgba(29, 29, 38, 0.8); 43 | font-weight: bold; 44 | } 45 | 46 | .unlogin-tips { 47 | margin-top: 6rpx; 48 | line-height: 33rpx; 49 | font-size: 24rpx; 50 | color: #8B8B8B; 51 | } 52 | 53 | .unlogin-btn { 54 | margin: 34rpx auto 0; 55 | width: 250rpx; 56 | height: 80rpx; 57 | line-height: 80rpx; 58 | background: #F5E069; 59 | outline: none; 60 | border: none; 61 | border-radius: 10rpx; 62 | font-size: 30rpx; 63 | text-align: center; 64 | color: #34373D; 65 | font-weight: bold; 66 | } 67 | 68 | .unlogin-btn::after { 69 | border: none; 70 | } 71 | -------------------------------------------------------------------------------- /client/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 小程序配置文件 3 | */ 4 | 5 | // 此处主机域名修改成腾讯云解决方案分配的域名 6 | var host = 'https://ty1qcd36.qcloud.la'; 7 | 8 | var config = { 9 | 10 | // 下面的地址配合云端 Demo 工作 11 | service: { 12 | host, 13 | 14 | // 登录地址,用于建立会话 15 | loginUrl: `${host}/weapp/login`, 16 | 17 | // 测试的请求地址,用于测试会话 18 | requestUrl: `${host}/weapp/user`, 19 | 20 | // 测试的信道服务地址 21 | tunnelUrl: `${host}/weapp/tunnel`, 22 | 23 | // 上传图片接口 24 | uploadUrl: `${host}/weapp/upload`, 25 | 26 | // 拉取商品列表 27 | productList: `${host}/weapp/product`, 28 | 29 | // 拉取商品详情 30 | productDetail: `${host}/weapp/product/`, 31 | 32 | // 拉取用户信息 33 | user: `${host}/weapp/user`, 34 | 35 | // 创建订单 36 | addOrder: `${host}/weapp/order`, 37 | 38 | // 获取已购买订单列表 39 | orderList: `${host}/weapp/order`, 40 | 41 | // 添加到购物车商品列表 42 | addTrolley: `${host}/weapp/trolley`, 43 | 44 | // 获取购物车商品列表 45 | trolleyList: `${host}/weapp/trolley`, 46 | 47 | // 更新购物车商品列表 48 | updateTrolley: `${host}/weapp/trolley`, 49 | 50 | // 添加评论 51 | addComment: `${host}/weapp/comment`, 52 | 53 | // 获取评论列表 54 | commentList: `${host}/weapp/comment`, 55 | 56 | } 57 | }; 58 | 59 | module.exports = config; 60 | -------------------------------------------------------------------------------- /client/images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/client/images/bg.png -------------------------------------------------------------------------------- /client/images/check-yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/client/images/check-yellow.png -------------------------------------------------------------------------------- /client/images/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/client/images/check.png -------------------------------------------------------------------------------- /client/images/discount.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/client/images/discount.png -------------------------------------------------------------------------------- /client/images/grey-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/client/images/grey-arrow.png -------------------------------------------------------------------------------- /client/images/home-sel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/client/images/home-sel.png -------------------------------------------------------------------------------- /client/images/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/client/images/home.png -------------------------------------------------------------------------------- /client/images/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/client/images/image.png -------------------------------------------------------------------------------- /client/images/line-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/client/images/line-black.png -------------------------------------------------------------------------------- /client/images/line-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/client/images/line-red.png -------------------------------------------------------------------------------- /client/images/order-sel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/client/images/order-sel.png -------------------------------------------------------------------------------- /client/images/order.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/client/images/order.png -------------------------------------------------------------------------------- /client/images/trolley-sel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/client/images/trolley-sel.png -------------------------------------------------------------------------------- /client/images/trolley.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/client/images/trolley.png -------------------------------------------------------------------------------- /client/images/user-sel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/client/images/user-sel.png -------------------------------------------------------------------------------- /client/images/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/client/images/user.png -------------------------------------------------------------------------------- /client/pages/add-comment/add-comment.js: -------------------------------------------------------------------------------- 1 | // pages/add-comment/add-comment.js 2 | const qcloud = require('../../vendor/wafer2-client-sdk/index') 3 | const config = require('../../config') 4 | Page({ 5 | 6 | /** 7 | * 页面的初始数据 8 | */ 9 | data: { 10 | product: {}, 11 | commentValue: '', 12 | commentImages: [], 13 | }, 14 | 15 | uploadImage(cb) { 16 | let commentImages = this.data.commentImages 17 | let images = [] 18 | 19 | if (commentImages.length) { 20 | let length = commentImages.length 21 | for (let i = 0; i < length; i++) { 22 | wx.uploadFile({ 23 | url: config.service.uploadUrl, 24 | filePath: commentImages[i], 25 | name: 'file', 26 | success: res => { 27 | let data = JSON.parse(res.data) 28 | length-- 29 | 30 | if (!data.code) { 31 | images.push(data.data.imgUrl) 32 | } 33 | 34 | if (length <= 0) { 35 | cb && cb(images) 36 | } 37 | }, 38 | fail: () => { 39 | length-- 40 | } 41 | }) 42 | } 43 | } else { 44 | cb && cb(images) 45 | } 46 | }, 47 | 48 | onInput(event) { 49 | this.setData({ 50 | commentValue: event.detail.value.trim() 51 | }) 52 | }, 53 | 54 | chooseImage() { 55 | let currentImages = this.data.commentImages 56 | 57 | wx.chooseImage({ 58 | count: 3, 59 | sizeType: ['compressed'], 60 | sourceType: ['album', 'camera'], 61 | success: res => { 62 | 63 | currentImages = currentImages.concat(res.tempFilePaths) 64 | 65 | let end = currentImages.length 66 | let begin = Math.max(end - 3, 0) 67 | currentImages = currentImages.slice(begin, end) 68 | 69 | this.setData({ 70 | commentImages: currentImages 71 | }) 72 | 73 | }, 74 | }) 75 | }, 76 | 77 | previewImg(event) { 78 | let target = event.currentTarget 79 | let src = target.dataset.src 80 | 81 | wx.previewImage({ 82 | current: src, 83 | urls: this.data.commentImages 84 | }) 85 | }, 86 | 87 | addComment(event) { 88 | let content = this.data.commentValue 89 | if (!content) return 90 | 91 | wx.showLoading({ 92 | title: '正在发表评论' 93 | }) 94 | 95 | this.uploadImage(images => { 96 | qcloud.request({ 97 | url: config.service.addComment, 98 | login: true, 99 | method: 'PUT', 100 | data: { 101 | images, 102 | content, 103 | product_id: this.data.product.id 104 | }, 105 | success: result => { 106 | wx.hideLoading() 107 | 108 | let data = result.data 109 | 110 | if (!data.code) { 111 | wx.showToast({ 112 | title: '发表评论成功' 113 | }) 114 | 115 | setTimeout(() => { 116 | wx.navigateBack() 117 | }, 1500) 118 | } else { 119 | wx.showToast({ 120 | icon: 'none', 121 | title: '发表评论失败' 122 | }) 123 | } 124 | }, 125 | fail: () => { 126 | wx.hideLoading() 127 | 128 | wx.showToast({ 129 | icon: 'none', 130 | title: '发表评论失败' 131 | }) 132 | } 133 | }) 134 | }) 135 | }, 136 | 137 | /** 138 | * 生命周期函数--监听页面加载 139 | */ 140 | onLoad: function (options) { 141 | let product = { 142 | id: options.id, 143 | name: options.name, 144 | price: options.price, 145 | image: options.image 146 | } 147 | this.setData({ 148 | product: product 149 | }) 150 | }, 151 | 152 | /** 153 | * 生命周期函数--监听页面初次渲染完成 154 | */ 155 | onReady: function () { 156 | 157 | }, 158 | 159 | /** 160 | * 生命周期函数--监听页面显示 161 | */ 162 | onShow: function () { 163 | 164 | }, 165 | 166 | /** 167 | * 生命周期函数--监听页面隐藏 168 | */ 169 | onHide: function () { 170 | 171 | }, 172 | 173 | /** 174 | * 生命周期函数--监听页面卸载 175 | */ 176 | onUnload: function () { 177 | 178 | }, 179 | 180 | /** 181 | * 页面相关事件处理函数--监听用户下拉动作 182 | */ 183 | onPullDownRefresh: function () { 184 | 185 | }, 186 | 187 | /** 188 | * 页面上拉触底事件的处理函数 189 | */ 190 | onReachBottom: function () { 191 | 192 | }, 193 | 194 | /** 195 | * 用户点击右上角分享 196 | */ 197 | onShareAppMessage: function () { 198 | 199 | } 200 | }) -------------------------------------------------------------------------------- /client/pages/add-comment/add-comment.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "添加评论" 3 | } -------------------------------------------------------------------------------- /client/pages/add-comment/add-comment.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{product.name}} 6 | ¥ {{product.price}} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 提交评价 -------------------------------------------------------------------------------- /client/pages/add-comment/add-comment.wxss: -------------------------------------------------------------------------------- 1 | /* pages/add-comment/add-comment.wxss */ 2 | .product-card { 3 | display: flex; 4 | align-items: center; 5 | padding-left: 26rpx; 6 | height: 200rpx; 7 | background: #FFFFFF; 8 | } 9 | 10 | .product-image { 11 | flex-shrink: 0; 12 | width: 160rpx; 13 | height: 160rpx; 14 | } 15 | 16 | .product-info { 17 | flex: 1; 18 | padding: 27rpx; 19 | height: 100%; 20 | box-sizing: border-box; 21 | font-size: 28rpx; 22 | line-height: 40rpx; 23 | } 24 | 25 | .product-name { 26 | color: rgba(29, 29, 38, 0.8); 27 | } 28 | 29 | .product-price { 30 | margin-top: 12rpx; 31 | font-weight: bold; 32 | } 33 | 34 | .comment-cnt { 35 | margin: 29rpx 27rpx 0; 36 | padding-bottom: 26rpx; 37 | background: #FFFFFF; 38 | box-shadow: 0 2rpx 13rpx 5rpx rgba(0, 0, 0, 0.02); 39 | border-radius: 10rpx; 40 | } 41 | 42 | .comment-ipt { 43 | width: 100%; 44 | height: 207rpx; 45 | padding: 29rpx 34rpx; 46 | box-sizing: border-box; 47 | font-size: 28rpx; 48 | line-height: 40rpx; 49 | } 50 | 51 | .comment-placeholder { 52 | color: rgba(29, 29, 38, 0.4); 53 | } 54 | 55 | .preview-cnt { 56 | display: flex; 57 | margin-top: 29rpx; 58 | margin-left: 29rpx; 59 | } 60 | 61 | .preview-image { 62 | margin-right: 10rpx; 63 | width: 180rpx; 64 | height: 180rpx; 65 | border-radius: 5rpx; 66 | } 67 | 68 | .opr-upload { 69 | display: block; 70 | margin-top: 29rpx; 71 | margin-left: 29rpx; 72 | width: 43rpx; 73 | height: 43rpx; 74 | } 75 | 76 | .comment-btn { 77 | margin: 34rpx auto; 78 | width: 250rpx; 79 | height: 80rpx; 80 | line-height: 80rpx; 81 | text-align: center; 82 | background: #F5E069; 83 | border-radius: 10rpx; 84 | font-size: 30rpx; 85 | color: #34373D; 86 | } 87 | 88 | .comment-btn.dis { 89 | color: rgba(52, 55, 61, 0.5); 90 | } -------------------------------------------------------------------------------- /client/pages/comment/comment.js: -------------------------------------------------------------------------------- 1 | const qcloud = require('../../vendor/wafer2-client-sdk/index') 2 | const config = require('../../config') 3 | const _ = require('../../utils/util') 4 | 5 | Page({ 6 | 7 | /** 8 | * 页面的初始数据 9 | */ 10 | data: { 11 | commentList: [], // 评论列表 12 | }, 13 | 14 | previewImg(event) { 15 | let target = event.currentTarget 16 | let src = target.dataset.src 17 | let urls = target.dataset.urls 18 | 19 | wx.previewImage({ 20 | current: src, 21 | urls: urls 22 | }) 23 | }, 24 | 25 | getCommentList(id) { 26 | qcloud.request({ 27 | url: config.service.commentList, 28 | data: { 29 | product_id: id 30 | }, 31 | success: result => { 32 | let data = result.data 33 | if (!data.code) { 34 | this.setData({ 35 | commentList: data.data.map(item => { 36 | let itemDate = new Date(item.create_time) 37 | item.createTime = _.formatTime(itemDate) 38 | item.images = item.images ? item.images.split(';;') : [] 39 | return item 40 | }) 41 | }) 42 | } 43 | }, 44 | }) 45 | }, 46 | 47 | /** 48 | * 生命周期函数--监听页面加载 49 | */ 50 | onLoad: function (options) { 51 | let product = { 52 | id: options.id, 53 | name: options.name, 54 | price: options.price, 55 | image: options.image 56 | } 57 | this.setData({ 58 | product: product 59 | }) 60 | this.getCommentList(product.id) 61 | }, 62 | 63 | /** 64 | * 生命周期函数--监听页面初次渲染完成 65 | */ 66 | onReady: function () { 67 | 68 | }, 69 | 70 | /** 71 | * 生命周期函数--监听页面显示 72 | */ 73 | onShow: function () { 74 | 75 | }, 76 | 77 | /** 78 | * 生命周期函数--监听页面隐藏 79 | */ 80 | onHide: function () { 81 | 82 | }, 83 | 84 | /** 85 | * 生命周期函数--监听页面卸载 86 | */ 87 | onUnload: function () { 88 | 89 | }, 90 | 91 | /** 92 | * 页面相关事件处理函数--监听用户下拉动作 93 | */ 94 | onPullDownRefresh: function () { 95 | 96 | }, 97 | 98 | /** 99 | * 页面上拉触底事件的处理函数 100 | */ 101 | onReachBottom: function () { 102 | 103 | }, 104 | 105 | /** 106 | * 用户点击右上角分享 107 | */ 108 | onShareAppMessage: function () { 109 | 110 | } 111 | }) -------------------------------------------------------------------------------- /client/pages/comment/comment.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "用户评论" 3 | } -------------------------------------------------------------------------------- /client/pages/comment/comment.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{product.name}} 6 | ¥ {{product.price}} 7 | 8 | 9 | 10 | {{commentList.length}}条评价 11 | 12 | 13 | 14 | 15 | {{item.username}} 16 | {{item.createTime}} 17 | 18 | {{item.content}} 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /client/pages/comment/comment.wxss: -------------------------------------------------------------------------------- 1 | .product-card { 2 | display: flex; 3 | align-items: center; 4 | padding-left: 26rpx; 5 | height: 200rpx; 6 | background: #FFFFFF; 7 | } 8 | 9 | .product-image { 10 | flex-shrink: 0; 11 | width: 160rpx; 12 | height: 160rpx; 13 | } 14 | 15 | .product-info { 16 | flex: 1; 17 | padding: 27rpx; 18 | height: 100%; 19 | box-sizing: border-box; 20 | font-size: 28rpx; 21 | line-height: 40rpx; 22 | } 23 | 24 | .product-name { 25 | color: rgba(29, 29, 38, 0.8); 26 | } 27 | 28 | .product-price { 29 | margin-top: 12rpx; 30 | font-weight: bold; 31 | } 32 | 33 | .comment-list { 34 | margin-top: 16rpx; 35 | background: #FFFFFF; 36 | box-shadow: 0 2rpx 13rpx 5rpx rgba(0, 0, 0, 0.02); 37 | } 38 | 39 | .comment-title { 40 | padding: 25rpx 26rpx 14rpx; 41 | font-size: 30rpx; 42 | line-height: 42rpx; 43 | color: rgba(29, 29, 38, 0.8); 44 | font-weight: bold; 45 | } 46 | 47 | .comment-card { 48 | display: flex; 49 | padding: 30rpx 23rpx 0 26rpx; 50 | } 51 | 52 | .comment-card:last-child .comment-cnt { 53 | border-bottom: none; 54 | } 55 | 56 | .comment-avatar { 57 | flex-shrink: 0; 58 | width: 60rpx; 59 | height: 60rpx; 60 | border-radius: 50%; 61 | } 62 | 63 | .comment-cnt { 64 | flex: 1; 65 | margin-left: 24rpx; 66 | padding-bottom: 30rpx; 67 | border-bottom: 1px solid rgba(151, 151, 151, 0.2); 68 | } 69 | 70 | .comment-top { 71 | display: flex; 72 | font-size: 25rpx; 73 | line-height: 36rpx; 74 | color: rgba(29, 29, 38, 0.5); 75 | } 76 | 77 | .comment-username { 78 | flex: 1; 79 | } 80 | 81 | .comment-content { 82 | font-size: 28rpx; 83 | line-height: 40rpx; 84 | color: #1D1D26; 85 | } 86 | 87 | .preview-list { 88 | display: flex; 89 | margin-top: 16rpx; 90 | } 91 | 92 | .preview-item { 93 | margin-right: 10rpx; 94 | width: 180rpx; 95 | height: 180rpx; 96 | border-radius: 10rpx; 97 | } -------------------------------------------------------------------------------- /client/pages/detail/detail.js: -------------------------------------------------------------------------------- 1 | // pages/detail/detail.js 2 | 3 | const qcloud = require('../../vendor/wafer2-client-sdk/index') 4 | const config = require('../../config') 5 | const _ = require('../../utils/util') 6 | 7 | Page({ 8 | 9 | /** 10 | * 页面的初始数据 11 | */ 12 | data: { 13 | product: {}, 14 | }, 15 | 16 | getProduct(id){ 17 | wx.showLoading({ 18 | title: '商品数据加载中...', 19 | }) 20 | 21 | qcloud.request({ 22 | url: config.service.productDetail + id, 23 | success: result => { 24 | wx.hideLoading() 25 | 26 | let data = result.data 27 | console.log(data); 28 | 29 | if (!data.code) { 30 | this.setData({ 31 | product: data.data 32 | }) 33 | } else { 34 | setTimeout(() => { 35 | wx.navigateBack() 36 | }, 2000) 37 | } 38 | }, 39 | fail: () => { 40 | wx.hideLoading() 41 | 42 | setTimeout(() => { 43 | wx.navigateBack() 44 | }, 2000) 45 | } 46 | }) 47 | }, 48 | 49 | buy(){ 50 | wx.showLoading({ 51 | title: '商品购买中...', 52 | }) 53 | 54 | let product = Object.assign({ 55 | count: 1 56 | }, this.data.product) 57 | 58 | qcloud.request({ 59 | url: config.service.addOrder, 60 | login: true, 61 | method: 'POST', 62 | data: { 63 | list: [product], 64 | isInstantBuy: true 65 | }, 66 | success: result => { 67 | wx.hideLoading() 68 | 69 | let data = result.data 70 | 71 | if (!data.code) { 72 | wx.showToast({ 73 | title: '商品购买成功', 74 | }) 75 | } else { 76 | wx.showToast({ 77 | icon: 'none', 78 | title: '商品购买失败', 79 | }) 80 | } 81 | }, 82 | fail: () => { 83 | wx.hideLoading() 84 | 85 | wx.showToast({ 86 | icon: 'none', 87 | title: '商品购买失败', 88 | }) 89 | } 90 | }) 91 | }, 92 | 93 | addToTrolley(){ 94 | wx.showLoading({ 95 | title: '正在添加到购物车...', 96 | }) 97 | 98 | qcloud.request({ 99 | url: config.service.addTrolley, 100 | login: true, 101 | method: 'PUT', 102 | data: this.data.product, 103 | success: result => { 104 | wx.hideLoading() 105 | 106 | let data = result.data 107 | 108 | if (!data.code){ 109 | wx.showToast({ 110 | title: '已添加到购物车', 111 | }) 112 | } else { 113 | wx.showToast({ 114 | icon: 'none', 115 | title: '添加到购物车失败', 116 | }) 117 | } 118 | }, 119 | fail: () => { 120 | wx.hideLoading() 121 | 122 | wx.showToast({ 123 | icon: 'none', 124 | title: '添加到购物车失败', 125 | }) 126 | } 127 | }) 128 | 129 | }, 130 | 131 | onTapCommentEntry() { 132 | let product = this.data.product 133 | if (product.commentCount) { 134 | wx.navigateTo({ 135 | url: `/pages/comment/comment?id=${product.id}&price=${product.price}&name=${product.name}&image=${product.image}` 136 | }) 137 | } 138 | }, 139 | 140 | /** 141 | * 生命周期函数--监听页面加载 142 | */ 143 | onLoad: function (options) { 144 | this.getProduct(options.id) 145 | }, 146 | 147 | /** 148 | * 生命周期函数--监听页面初次渲染完成 149 | */ 150 | onReady: function () { 151 | 152 | }, 153 | 154 | /** 155 | * 生命周期函数--监听页面显示 156 | */ 157 | onShow: function () { 158 | 159 | }, 160 | 161 | /** 162 | * 生命周期函数--监听页面隐藏 163 | */ 164 | onHide: function () { 165 | 166 | }, 167 | 168 | /** 169 | * 生命周期函数--监听页面卸载 170 | */ 171 | onUnload: function () { 172 | 173 | }, 174 | 175 | /** 176 | * 页面相关事件处理函数--监听用户下拉动作 177 | */ 178 | onPullDownRefresh: function () { 179 | 180 | }, 181 | 182 | /** 183 | * 页面上拉触底事件的处理函数 184 | */ 185 | onReachBottom: function () { 186 | 187 | }, 188 | 189 | /** 190 | * 用户点击右上角分享 191 | */ 192 | onShareAppMessage: function () { 193 | 194 | } 195 | }) -------------------------------------------------------------------------------- /client/pages/detail/detail.json: -------------------------------------------------------------------------------- 1 | {"navigationBarTitleText": "商品详情"} -------------------------------------------------------------------------------- /client/pages/detail/detail.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{product.name}} 5 | {{product.source}} 6 | 7 | 8 | {{product.price}} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 7天免费退货 16 | 17 | 18 | 19 | 24小时内发货并配送运费险 20 | 21 | 22 | 23 | 24 | 已买评价 25 | {{product.commentCount}}条 26 | 27 | 28 | “{{product.firstComment.content}}” 29 | 30 | 31 | 32 | 加入购物车 33 | 立即购买 34 | -------------------------------------------------------------------------------- /client/pages/detail/detail.wxss: -------------------------------------------------------------------------------- 1 | /* pages/detail/detail.wxss */ 2 | 3 | .product-card { 4 | padding-bottom: 15rpx; 5 | background: #FFFFFF; 6 | box-shadow: 0 2rpx 13rpx 5rpx rgba(0, 0, 0, 0.02); 7 | } 8 | 9 | .product-image { 10 | display: block; 11 | margin: 0 auto; 12 | width: 370rpx; 13 | height: 370rpx; 14 | } 15 | 16 | .product-name { 17 | margin: 42rpx 49rpx 0; 18 | font-size: 38rpx; 19 | line-height: 53rpx; 20 | color: rgba(29, 29, 38, 0.8); 21 | font-weight: bold; 22 | } 23 | 24 | 25 | .product-source { 26 | margin: 5rpx 49rpx 0; 27 | font-size: 28rpx; 28 | line-height: 40rpx; 29 | color: #8B8B8B; 30 | } 31 | 32 | 33 | .product-meta { 34 | display: flex; 35 | align-items: center; 36 | justify-content: flex-end; 37 | margin-right: 29rpx; 38 | } 39 | 40 | .product-money { 41 | width: 47rpx; 42 | height: 47rpx; 43 | line-height: 47rpx; 44 | text-align: center; 45 | border-radius: 50%; 46 | color: #34373D; 47 | font-size: 34rpx; 48 | font-weight: bold; 49 | background: #F7E687; 50 | } 51 | 52 | .product-price { 53 | margin: 0 10rpx; 54 | font-size: 38rpx; 55 | } 56 | 57 | .info-card { 58 | margin: 26rpx 27rpx; 59 | padding: 0 26rpx; 60 | background: #FFFFFF; 61 | box-shadow: 0 2rpx 13rpx 5rpx rgba(0, 0, 0, 0.02); 62 | border-radius: 13rpx; 63 | } 64 | 65 | .advantage-list { 66 | padding: 43rpx 15rpx 39rpx; 67 | border-bottom: 1px solid rgba(151, 151, 151, 0.2); 68 | } 69 | 70 | .advantage-item { 71 | display: flex; 72 | align-items: center; 73 | } 74 | 75 | .advantage-icon { 76 | width: 22rpx; 77 | height: 16rpx; 78 | } 79 | 80 | .advantage-text { 81 | margin-left: 15rpx; 82 | font-size: 28rpx; 83 | color: #34373D; 84 | line-height: 47rpx; 85 | } 86 | 87 | .comment-entry { 88 | padding: 26rpx 10rpx 21rpx; 89 | } 90 | 91 | .comment-btn { 92 | display: flex; 93 | align-items: center; 94 | } 95 | 96 | .comment-title { 97 | font-size: 30rpx; 98 | line-height: 42rpx; 99 | color: rgba(29, 29, 38, 0.8); 100 | font-weight: bold; 101 | } 102 | 103 | .comment-count { 104 | flex: 1; 105 | margin-right: 16rpx; 106 | text-align: right; 107 | font-size: 28rpx; 108 | line-height: 40rpx; 109 | color: #8B8B8B; 110 | } 111 | 112 | .comment-arrow { 113 | width: 11rpx; 114 | height: 18rpx; 115 | } 116 | 117 | .comment-preview { 118 | margin-top: 26rpx; 119 | font-size: 28rpx; 120 | line-height: 40rpx; 121 | color: #8B8B8B; 122 | } 123 | 124 | .opr-cnt { 125 | position: fixed; 126 | bottom: 0; 127 | left: 0; 128 | right: 0; 129 | display: flex; 130 | align-items: center; 131 | } 132 | 133 | .opr-trolley, 134 | .opr-buy { 135 | flex: 1; 136 | height: 100rpx; 137 | line-height: 100rpx; 138 | text-align: center; 139 | font-size: 30rpx; 140 | color: #34373D; 141 | font-weight: bold; 142 | } 143 | 144 | .opr-trolley { 145 | background: rgba(245, 224, 105, 0.6); 146 | } 147 | 148 | .opr-buy { 149 | background: #F5E069; 150 | } -------------------------------------------------------------------------------- /client/pages/home/home.js: -------------------------------------------------------------------------------- 1 | // pages/home/home.js 2 | const qcloud = require('../../vendor/wafer2-client-sdk/index') 3 | const config = require('../../config.js') 4 | 5 | Page({ 6 | 7 | /** 8 | * 页面的初始数据 9 | */ 10 | data: { 11 | productList: [], // 商品列表 12 | }, 13 | 14 | getProductList(){ 15 | wx.showLoading({ 16 | title: '商品数据加载中...', 17 | }) 18 | 19 | qcloud.request({ 20 | url: config.service.productList, 21 | success: result => { 22 | wx.hideLoading() 23 | 24 | let data = result.data 25 | if (!data.code) { 26 | this.setData({ 27 | productList: data.data 28 | }) 29 | } else { 30 | wx.showToast({ 31 | icon: 'none', 32 | title: '商品数据加载错误', 33 | }) 34 | } 35 | }, 36 | 37 | fail: () => { 38 | wx.hideLoading() 39 | 40 | wx.showToast({ 41 | icon: 'none', 42 | title: '商品数据加载错误', 43 | }) 44 | } 45 | }) 46 | }, 47 | 48 | addToTrolley(event){ 49 | let productId = event.currentTarget.dataset.id 50 | let productList = this.data.productList 51 | let product 52 | 53 | for (let i = 0, len = productList.length; i < len; i++) { 54 | if (productList[i].id === productId) { 55 | product = productList[i] 56 | break 57 | } 58 | } 59 | 60 | if (product){ 61 | qcloud.request({ 62 | url: config.service.addTrolley, 63 | login: true, 64 | method: 'PUT', 65 | data: product, 66 | success: result => { 67 | let data = result.data 68 | 69 | if (!data.code) { 70 | wx.showToast({ 71 | title: '已添加到购物车', 72 | }) 73 | } else { 74 | wx.showToast({ 75 | icon: 'none', 76 | title: '添加到购物车失败', 77 | }) 78 | } 79 | }, 80 | fail: () => { 81 | wx.showToast({ 82 | icon: 'none', 83 | title: '添加到购物车失败', 84 | }) 85 | } 86 | }) 87 | 88 | } 89 | 90 | 91 | }, 92 | 93 | /** 94 | * 生命周期函数--监听页面加载 95 | */ 96 | onLoad: function (options) { 97 | this.getProductList() 98 | }, 99 | 100 | /** 101 | * 生命周期函数--监听页面初次渲染完成 102 | */ 103 | onReady: function () { 104 | 105 | }, 106 | 107 | /** 108 | * 生命周期函数--监听页面显示 109 | */ 110 | onShow: function () { 111 | 112 | }, 113 | 114 | /** 115 | * 生命周期函数--监听页面隐藏 116 | */ 117 | onHide: function () { 118 | 119 | }, 120 | 121 | /** 122 | * 生命周期函数--监听页面卸载 123 | */ 124 | onUnload: function () { 125 | 126 | }, 127 | 128 | /** 129 | * 页面相关事件处理函数--监听用户下拉动作 130 | */ 131 | onPullDownRefresh: function () { 132 | 133 | }, 134 | 135 | /** 136 | * 页面上拉触底事件的处理函数 137 | */ 138 | onReachBottom: function () { 139 | 140 | }, 141 | 142 | /** 143 | * 用户点击右上角分享 144 | */ 145 | onShareAppMessage: function () { 146 | 147 | } 148 | }) -------------------------------------------------------------------------------- /client/pages/home/home.json: -------------------------------------------------------------------------------- 1 | {"navigationBarTitleText": "商城首页"} -------------------------------------------------------------------------------- /client/pages/home/home.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{productList[0].name}} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 春季推荐 15 | 16 | 17 | 18 | 19 | 20 | 21 | {{pitem.name}} 22 | 23 | 24 | {{pitem.price}} 25 | + 26 | 27 | 28 | -------------------------------------------------------------------------------- /client/pages/home/home.wxss: -------------------------------------------------------------------------------- 1 | /* pages/home/home.wxss */ 2 | 3 | .hot-card { 4 | position: relative; 5 | margin: 32rpx 52rpx 0; 6 | height: 326rpx; 7 | background: #FFFFFF; 8 | box-shadow: 0 2rpx 13rpx 5rpx rgba(0, 0, 0, 0.02); 9 | border-radius: 13rpx; 10 | } 11 | 12 | .hot-name { 13 | position: absolute; 14 | display: flex; 15 | align-items: center; 16 | left: 44rpx; 17 | top: 41rpx; 18 | width: 300rpx; 19 | } 20 | 21 | .hot-name-line { 22 | width: 24rpx; 23 | height: 39rpx; 24 | } 25 | 26 | .hot-name-text { 27 | margin: 0 20rpx; 28 | font-size: 28rpx; 29 | color: #34373D; 30 | font-weight: bold; 31 | } 32 | 33 | .hot-info { 34 | position: absolute; 35 | width: 259rpx; 36 | height: 188rpx; 37 | left: 42rpx; 38 | bottom: 0; 39 | } 40 | 41 | .hot-image { 42 | position: absolute; 43 | top: 53rpx; 44 | right: 47rpx; 45 | width: 241rpx; 46 | height: 241rpx; 47 | } 48 | 49 | .list-title { 50 | display: flex; 51 | align-items: center; 52 | justify-content: center; 53 | margin: 90rpx 0 35rpx; 54 | } 55 | 56 | .list-title-line { 57 | width: 21rpx; 58 | height: 34rpx; 59 | } 60 | 61 | .list-title-text { 62 | margin: 0 20rpx; 63 | font-size: 28rpx; 64 | font-weight: bold; 65 | } 66 | 67 | .product-row { 68 | display: flex; 69 | justify-content: space-around; 70 | margin: 0 52rpx 28rpx; 71 | } 72 | 73 | .product-card { 74 | flex: 1; 75 | background: #FFFFFF; 76 | box-shadow: 0 2rpx 13rpx 5rpx rgba(0, 0, 0, 0.02); 77 | border-radius: 13rpx; 78 | } 79 | 80 | .product-card:first-child { 81 | margin-right: 27rpx; 82 | } 83 | 84 | .product-image { 85 | width: 100%; 86 | border-radius: 13rpx 13rpx 0 0; 87 | } 88 | 89 | .product-name { 90 | margin: 0 21rpx; 91 | font-size: 28rpx; 92 | color: rgba(29, 29, 38, 0.8); 93 | line-height: 40rpx; 94 | } 95 | 96 | .product-meta { 97 | display: flex; 98 | align-items: center; 99 | margin: 9rpx 21rpx 15rpx; 100 | } 101 | 102 | .product-money, 103 | .product-add { 104 | width: 36rpx; 105 | height: 36rpx; 106 | line-height: 36rpx; 107 | text-align: center; 108 | border-radius: 50%; 109 | color: #34373D; 110 | font-weight: 900; 111 | background: #F7E687; 112 | } 113 | 114 | .product-money { 115 | font-size: 24rpx; 116 | } 117 | 118 | .product-add { 119 | font-size: 32rpx; 120 | } 121 | 122 | .product-price { 123 | flex: 1; 124 | margin: 0 10rpx; 125 | font-size: 28rpx; 126 | } -------------------------------------------------------------------------------- /client/pages/order/order.js: -------------------------------------------------------------------------------- 1 | // pages/order/order.js 2 | const qcloud = require('../../vendor/wafer2-client-sdk/index') 3 | const config = require('../../config') 4 | const app = getApp() 5 | 6 | Page({ 7 | 8 | /** 9 | * 页面的初始数据 10 | */ 11 | data: { 12 | userInfo: null, 13 | orderList: [], // 订单列表 14 | }, 15 | 16 | onTapLogin() { 17 | app.login({ 18 | success: ({ userInfo }) => { 19 | this.setData({ 20 | userInfo 21 | }) 22 | } 23 | }) 24 | 25 | this.getOrder() 26 | }, 27 | 28 | getOrder() { 29 | wx.showLoading({ 30 | title: '刷新订单数据...', 31 | }) 32 | 33 | qcloud.request({ 34 | url: config.service.orderList, 35 | login: true, 36 | success: result => { 37 | wx.hideLoading() 38 | 39 | let data = result.data 40 | console.log(data) 41 | if(!data.code){ 42 | this.setData({ 43 | orderList: data.data 44 | }) 45 | } else { 46 | wx.showToast({ 47 | icon: 'none', 48 | title: '刷新订单数据失败', 49 | }) 50 | } 51 | }, 52 | fail: () => { 53 | wx.hideLoading() 54 | 55 | wx.showToast({ 56 | icon: 'none', 57 | title: '刷新订单数据失败', 58 | }) 59 | } 60 | }) 61 | }, 62 | 63 | /** 64 | * 生命周期函数--监听页面加载 65 | */ 66 | onLoad: function (options) { 67 | 68 | }, 69 | 70 | /** 71 | * 生命周期函数--监听页面初次渲染完成 72 | */ 73 | onReady: function () { 74 | 75 | }, 76 | 77 | /** 78 | * 生命周期函数--监听页面显示 79 | */ 80 | onShow: function () { 81 | app.checkSession({ 82 | success: ({ userInfo }) => { 83 | this.setData({ 84 | userInfo 85 | }) 86 | this.getOrder() 87 | } 88 | }) 89 | }, 90 | 91 | /** 92 | * 生命周期函数--监听页面隐藏 93 | */ 94 | onHide: function () { 95 | 96 | }, 97 | 98 | /** 99 | * 生命周期函数--监听页面卸载 100 | */ 101 | onUnload: function () { 102 | 103 | }, 104 | 105 | /** 106 | * 页面相关事件处理函数--监听用户下拉动作 107 | */ 108 | onPullDownRefresh: function () { 109 | 110 | }, 111 | 112 | /** 113 | * 页面上拉触底事件的处理函数 114 | */ 115 | onReachBottom: function () { 116 | 117 | }, 118 | 119 | /** 120 | * 用户点击右上角分享 121 | */ 122 | onShareAppMessage: function () { 123 | 124 | } 125 | }) -------------------------------------------------------------------------------- /client/pages/order/order.json: -------------------------------------------------------------------------------- 1 | {"navigationBarTitleText": "订单"} -------------------------------------------------------------------------------- /client/pages/order/order.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 未登录 8 | 点击微信登录后可方便购物 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 已完成 17 | 18 | 19 | 20 | 21 | 22 | {{item.name}} 23 | ¥ {{item.price}} 24 | 25 | 26 | 评价 27 | x{{item.count}} 28 | 29 | 30 | 31 | 32 | 33 | 34 | 暂时还没有订单 35 | 36 | -------------------------------------------------------------------------------- /client/pages/order/order.wxss: -------------------------------------------------------------------------------- 1 | .order-head { 2 | display: flex; 3 | align-items: center; 4 | padding: 26rpx 29rpx 14rpx; 5 | } 6 | 7 | .order-head-line { 8 | width: 21rpx; 9 | height: 34rpx; 10 | } 11 | 12 | .order-head-text { 13 | margin-left: 10rpx; 14 | font-size: 24rpx; 15 | line-height: 33rpx; 16 | color: rgba(52, 55, 61, 0.8); 17 | } 18 | 19 | .order-block { 20 | margin: 0 26rpx 26rpx; 21 | background: #FFFFFF; 22 | box-shadow: 0 2rpx 13rpx 5rpx rgba(0, 0, 0, 0.02); 23 | border-radius: 13rpx; 24 | } 25 | 26 | .order-card { 27 | display: flex; 28 | align-items: center; 29 | margin-left: 19rpx; 30 | margin-right: 23rpx; 31 | height: 200rpx; 32 | border-bottom: 1px solid rgba(151, 151, 151, 0.2); 33 | } 34 | 35 | .order-card:last-child { 36 | border-bottom: none; 37 | } 38 | 39 | .order-image { 40 | flex-shrink: 0; 41 | width: 160rpx; 42 | height: 160rpx; 43 | } 44 | 45 | .order-info { 46 | flex: 1; 47 | padding: 27rpx; 48 | height: 100%; 49 | box-sizing: border-box; 50 | font-size: 28rpx; 51 | line-height: 40rpx; 52 | } 53 | 54 | .order-name { 55 | color: rgba(29, 29, 38, 0.8); 56 | } 57 | 58 | .order-price { 59 | margin-top: 12rpx; 60 | font-weight: bold; 61 | } 62 | 63 | .order-opr { 64 | flex-shrink: 0; 65 | margin-left: 59rpx; 66 | padding-top: 34rpx; 67 | height: 100%; 68 | box-sizing: border-box; 69 | font-size: 28rpx; 70 | } 71 | 72 | .order-btn { 73 | width: 100rpx; 74 | height: 52rpx; 75 | line-height: 52rpx; 76 | background: #F5E069; 77 | border-radius: 8rpx; 78 | text-align: center; 79 | } 80 | 81 | .order-count { 82 | margin-top: 49rpx; 83 | line-height: 40rpx; 84 | font-weight: bold; 85 | text-align: right; 86 | } 87 | 88 | .order-empty { 89 | display: flex; 90 | flex-direction: column; 91 | justify-content: center; 92 | align-items: center; 93 | margin: 37rpx 27rpx; 94 | height: 431rpx; 95 | background: #FFFFFF; 96 | box-shadow: 0 2rpx 13rpx 5rpx rgba(0, 0, 0, 0.02); 97 | border-radius: 13rpx; 98 | } 99 | 100 | .empty-image { 101 | margin-bottom: 29rpx; 102 | width: 90rpx; 103 | height: 90rpx; 104 | opacity: 0.4; 105 | } 106 | 107 | .empty-text { 108 | font-size: 28rpx; 109 | line-height: 40rpx; 110 | color: rgba(139, 139, 139, 0.8); 111 | } 112 | -------------------------------------------------------------------------------- /client/pages/trolley/trolley.js: -------------------------------------------------------------------------------- 1 | // pages/trolley/trolley.js 2 | const qcloud = require('../../vendor/wafer2-client-sdk/index') 3 | const config = require('../../config') 4 | const app = getApp() 5 | 6 | Page({ 7 | 8 | /** 9 | * 页面的初始数据 10 | */ 11 | data: { 12 | userInfo: null, 13 | trolleyList: [], // 购物车商品列表 14 | trolleyCheckMap: [], // 购物车中选中的id哈希表 15 | trolleyAccount: 0, // 购物车结算总价 16 | isTrolleyEdit: false, // 购物车是否处于编辑状态 17 | isTrolleyTotalCheck: false, // 购物车中商品是否全选 18 | }, 19 | 20 | onTapLogin() { 21 | app.login({ 22 | success: ({ userInfo }) => { 23 | this.setData({ 24 | userInfo 25 | }) 26 | 27 | this.getTrolley() 28 | } 29 | }) 30 | 31 | 32 | }, 33 | 34 | getTrolley(){ 35 | wx.showLoading({ 36 | title: '刷新购物车数据...', 37 | }) 38 | 39 | qcloud.request({ 40 | url: config.service.trolleyList, 41 | login: true, 42 | success: result => { 43 | wx.hideLoading() 44 | 45 | let data = result.data 46 | 47 | if (!data.code) { 48 | this.setData({ 49 | trolleyList: data.data 50 | }) 51 | } else { 52 | wx.showToast({ 53 | icon: 'none', 54 | title: '数据刷新失败', 55 | }) 56 | } 57 | }, 58 | fail: () => { 59 | wx.hideLoading() 60 | 61 | wx.showToast({ 62 | icon: 'none', 63 | title: '数据刷新失败', 64 | }) 65 | } 66 | }) 67 | }, 68 | 69 | onTapCheckSingle(event) { 70 | let checkId = event.currentTarget.dataset.id 71 | let trolleyCheckMap = this.data.trolleyCheckMap 72 | let trolleyList = this.data.trolleyList 73 | let isTrolleyTotalCheck = this.data.isTrolleyTotalCheck 74 | let trolleyAccount = this.data.trolleyAccount 75 | let numTotalProduct 76 | let numCheckedProduct = 0 77 | 78 | // 单项商品被选中/取消 79 | trolleyCheckMap[checkId] = !trolleyCheckMap[checkId] 80 | 81 | // 判断选中的商品个数是否需商品总数相等 82 | numTotalProduct = trolleyList.length 83 | trolleyCheckMap.forEach(checked => { 84 | numCheckedProduct = checked ? numCheckedProduct+1 : numCheckedProduct 85 | }) 86 | 87 | isTrolleyTotalCheck = (numTotalProduct === numCheckedProduct) ? true : false 88 | 89 | trolleyAccount = this.calcAccount(trolleyList, trolleyCheckMap) 90 | 91 | this.setData({ 92 | trolleyCheckMap, 93 | isTrolleyTotalCheck, 94 | trolleyAccount 95 | }) 96 | }, 97 | 98 | onTapCheckTotal(event) { 99 | let trolleyCheckMap = this.data.trolleyCheckMap 100 | let trolleyList = this.data.trolleyList 101 | let isTrolleyTotalCheck = this.data.isTrolleyTotalCheck 102 | let trolleyAccount = this.data.trolleyAccount 103 | 104 | // 全选按钮被选中/取消 105 | isTrolleyTotalCheck = !isTrolleyTotalCheck 106 | 107 | // 遍历并修改所有商品的状态 108 | trolleyList.forEach(product => { 109 | trolleyCheckMap[product.id] = isTrolleyTotalCheck 110 | }) 111 | 112 | trolleyAccount = this.calcAccount(trolleyList, trolleyCheckMap) 113 | 114 | this.setData({ 115 | isTrolleyTotalCheck, 116 | trolleyCheckMap, 117 | trolleyAccount 118 | }) 119 | 120 | }, 121 | 122 | calcAccount(trolleyList, trolleyCheckMap) { 123 | let account = 0 124 | trolleyList.forEach(product => { 125 | account = trolleyCheckMap[product.id] ? account + product.price * product.count : account 126 | }) 127 | 128 | return account 129 | }, 130 | 131 | onTapEditTrolley() { 132 | let isTrolleyEdit = this.data.isTrolleyEdit 133 | 134 | if (isTrolleyEdit) { 135 | this.updateTrolley() 136 | } else { 137 | this.setData({ 138 | isTrolleyEdit: !isTrolleyEdit 139 | }) 140 | } 141 | 142 | 143 | }, 144 | 145 | adjustTrolleyProductCount(event) { 146 | let trolleyCheckMap = this.data.trolleyCheckMap 147 | let trolleyList = this.data.trolleyList 148 | let dataset = event.currentTarget.dataset 149 | let adjustType = dataset.type 150 | let productId = dataset.id 151 | let product 152 | let index 153 | 154 | 155 | for (index = 0; index < trolleyList.length; index++) { 156 | if (productId === trolleyList[index].id) { 157 | product = trolleyList[index] 158 | break 159 | } 160 | } 161 | 162 | if (product) { 163 | if (adjustType === 'add') { 164 | // 点击加号 165 | product.count++ 166 | } else { 167 | // 点击减号 168 | if (product.count <= 1) { 169 | // 商品数量不超过1,点击减号相当于删除 170 | delete trolleyCheckMap[productId] 171 | trolleyList.splice(index, 1) 172 | } else { 173 | // 商品数量大于1 174 | product.count-- 175 | } 176 | } 177 | } 178 | 179 | // 调整结算总价 180 | let trolleyAccount = this.calcAccount(trolleyList, trolleyCheckMap) 181 | 182 | if (!trolleyList.length) { 183 | // 当购物车为空,自动同步至服务器 184 | this.updateTrolley() 185 | } 186 | 187 | this.setData({ 188 | trolleyAccount, 189 | trolleyList, 190 | trolleyCheckMap 191 | }) 192 | }, 193 | 194 | updateTrolley() { 195 | wx.showLoading({ 196 | title: '更新购物车数据...', 197 | }) 198 | 199 | let trolleyList = this.data.trolleyList 200 | 201 | qcloud.request({ 202 | url: config.service.updateTrolley, 203 | method: 'POST', 204 | login: true, 205 | data: { 206 | list: trolleyList 207 | }, 208 | success: result => { 209 | wx.hideLoading() 210 | 211 | let data = result.data 212 | 213 | if (!data.code) { 214 | this.setData({ 215 | isTrolleyEdit: false 216 | }) 217 | } else { 218 | wx.showToast({ 219 | icon: 'none', 220 | title: '更新购物车失败' 221 | }) 222 | } 223 | }, 224 | fail: () => { 225 | wx.hideLoading() 226 | 227 | wx.showToast({ 228 | icon: 'none', 229 | title: '更新购物车失败' 230 | }) 231 | } 232 | }) 233 | }, 234 | 235 | onTapPay() { 236 | if (!this.data.trolleyAccount) return 237 | 238 | wx.showLoading({ 239 | title: '结算中...', 240 | }) 241 | 242 | let trolleyCheckMap = this.data.trolleyCheckMap 243 | let trolleyList = this.data.trolleyList 244 | 245 | let needToPayProductList = trolleyList.filter(product => { 246 | return !!trolleyCheckMap[product.id] 247 | }) 248 | 249 | // 请求后台 250 | qcloud.request({ 251 | url: config.service.addOrder, 252 | login: true, 253 | method: 'POST', 254 | data: { 255 | list: needToPayProductList 256 | }, 257 | success: result => { 258 | wx.hideLoading() 259 | 260 | let data = result.data 261 | 262 | if (!data.code) { 263 | wx.showToast({ 264 | title: '结算成功', 265 | }) 266 | 267 | this.getTrolley() 268 | } else { 269 | wx.showToast({ 270 | icon: 'none', 271 | title: '结算失败', 272 | }) 273 | } 274 | }, 275 | fail: () => { 276 | wx.hideLoading() 277 | 278 | wx.showToast({ 279 | icon: 'none', 280 | title: '结算失败', 281 | }) 282 | } 283 | }) 284 | }, 285 | 286 | /** 287 | * 生命周期函数--监听页面加载 288 | */ 289 | onLoad: function (options) { 290 | 291 | }, 292 | 293 | /** 294 | * 生命周期函数--监听页面初次渲染完成 295 | */ 296 | onReady: function () { 297 | 298 | }, 299 | 300 | /** 301 | * 生命周期函数--监听页面显示 302 | */ 303 | onShow: function () { 304 | app.checkSession({ 305 | success: ({ userInfo }) => { 306 | this.setData({ 307 | userInfo 308 | }) 309 | this.getTrolley() 310 | } 311 | }) 312 | }, 313 | 314 | /** 315 | * 生命周期函数--监听页面隐藏 316 | */ 317 | onHide: function () { 318 | 319 | }, 320 | 321 | /** 322 | * 生命周期函数--监听页面卸载 323 | */ 324 | onUnload: function () { 325 | 326 | }, 327 | 328 | /** 329 | * 页面相关事件处理函数--监听用户下拉动作 330 | */ 331 | onPullDownRefresh: function () { 332 | 333 | }, 334 | 335 | /** 336 | * 页面上拉触底事件的处理函数 337 | */ 338 | onReachBottom: function () { 339 | 340 | }, 341 | 342 | /** 343 | * 用户点击右上角分享 344 | */ 345 | onShareAppMessage: function () { 346 | 347 | } 348 | }) -------------------------------------------------------------------------------- /client/pages/trolley/trolley.json: -------------------------------------------------------------------------------- 1 | {"navigationBarTitleText": "购物车"} -------------------------------------------------------------------------------- /client/pages/trolley/trolley.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 未登录 8 | 点击微信登录后可方便购物 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 全选 20 | 21 | {{isTrolleyEdit ? '完成' : '编辑'}} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{item.name}} 31 | ¥ {{item.price}} 32 | 33 | - 34 | {{item.count}} 35 | + 36 | 37 | x {{item.count}} 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 全选 47 | 48 | 52 | 结算 53 | 54 | 55 | 56 | 57 | 58 | 购物车是空的 59 | 快添加物品进来吧 60 | -------------------------------------------------------------------------------- /client/pages/trolley/trolley.wxss: -------------------------------------------------------------------------------- 1 | /* pages/trolley/trolley.wxss */ 2 | .trolley-empty { 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: center; 6 | align-items: center; 7 | margin: 37rpx 27rpx; 8 | height: 431rpx; 9 | background: #FFFFFF; 10 | box-shadow: 0 2rpx 13rpx 5rpx rgba(0, 0, 0, 0.02); 11 | border-radius: 13rpx; 12 | } 13 | 14 | .empty-image { 15 | margin-bottom: 29rpx; 16 | width: 90rpx; 17 | height: 90rpx; 18 | opacity: 0.4; 19 | } 20 | 21 | .empty-text { 22 | font-size: 28rpx; 23 | line-height: 40rpx; 24 | color: rgba(139, 139, 139, 0.8); 25 | } 26 | 27 | 28 | 29 | .trolley-top { 30 | display: flex; 31 | align-items: center; 32 | justify-content: space-between; 33 | padding-left: 24rpx; 34 | } 35 | 36 | .trolley-total { 37 | display: flex; 38 | align-items: center; 39 | padding-left: 35rpx; 40 | font-size: 32rpx; 41 | } 42 | 43 | .trolley-check-wrapper { 44 | flex-shrink: 0; 45 | display: flex; 46 | align-items: center; 47 | justify-content: center; 48 | width:36rpx; 49 | height: 36rpx; 50 | border-radius: 50%; 51 | background: #ECECEE; 52 | } 53 | 54 | .trolley-check-wrapper.white { 55 | background: #FFFFFF; 56 | } 57 | 58 | .trolley-check-wrapper.check { 59 | background: #F5E069; 60 | } 61 | 62 | .trolley-check { 63 | width: 22rpx; 64 | height: 16rpx; 65 | } 66 | 67 | .trolley-total-text, 68 | .trolley-edit { 69 | margin-left: 18rpx; 70 | margin-right: 33rpx; 71 | color: rgb(52, 55, 61, 0.8); 72 | font-size: 24rpx; 73 | line-height: 33rpx; 74 | } 75 | 76 | .product-list { 77 | position: absolute; 78 | left: 0; 79 | right: 0; 80 | top: 54rpx; 81 | bottom: 100rpx; 82 | padding-bottom: 30rpx; 83 | } 84 | 85 | .product-card { 86 | display: flex; 87 | align-items: center; 88 | margin: 0 26rpx 26rpx; 89 | padding-left: 32rpx; 90 | height: 200rpx; 91 | background: #FFFFFF; 92 | box-shadow: 0 2rpx 13rpx 5rpx rgba(0, 0, 0, 0.02); 93 | border-radius: 13rpx; 94 | } 95 | 96 | .product-info { 97 | flex: 1; 98 | padding: 38rpx 28rpx 0 36rpx; 99 | height: 100%; 100 | box-sizing: border-box; 101 | font-size: 28rpx; 102 | line-height: 40rpx; 103 | } 104 | 105 | .product-img { 106 | flex-shrink: 0; 107 | margin-left: 31rpx; 108 | width: 160rpx; 109 | height: 160rpx; 110 | } 111 | 112 | .product-name { 113 | color: rgba(29, 29, 38, 0.8); 114 | } 115 | 116 | .product-price { 117 | margin-top: 3rpx; 118 | } 119 | 120 | .product-count { 121 | text-align: right; 122 | } 123 | 124 | .product-count-edit { 125 | display: flex; 126 | align-items: center; 127 | justify-content: flex-end; 128 | } 129 | 130 | .count-minus, 131 | .count-add { 132 | width: 36rpx; 133 | height: 36rpx; 134 | line-height: 36rpx; 135 | border-radius: 50%; 136 | border: 3rpx solid #F6E474; 137 | text-align: center; 138 | font-size: 28rpx; 139 | font-weight: 900; 140 | } 141 | 142 | .count-now { 143 | margin: 0 18rpx; 144 | } 145 | 146 | .trolley-account { 147 | display: flex; 148 | align-items: center; 149 | position: absolute; 150 | left: 0; 151 | bottom: 0; 152 | right: 0; 153 | padding-left: 24rpx; 154 | height: 100rpx; 155 | background: #fff; 156 | } 157 | 158 | .trolley-account-text { 159 | flex: 1; 160 | display: flex; 161 | align-items: center; 162 | justify-content: flex-end; 163 | padding-right: 29rpx; 164 | color: rgba(52, 55, 61, 0.8); 165 | font-size: 24rpx; 166 | line-height: 33rpx; 167 | } 168 | 169 | .trolley-account-now { 170 | margin-left: 12rpx; 171 | font-size: 34rpx; 172 | line-height: 48rpx; 173 | color: #000000; 174 | } 175 | 176 | .pay-btn { 177 | width: 250rpx; 178 | height: 100%; 179 | background: #F5E069; 180 | color: rgba(52, 55, 61, 0.5); 181 | line-height: 100rpx; 182 | text-align: center; 183 | font-size: 30rpx; 184 | } 185 | 186 | .pay-btn.canpay { 187 | color: #34373D; 188 | } 189 | -------------------------------------------------------------------------------- /client/pages/user/user.js: -------------------------------------------------------------------------------- 1 | // pages/user/user.js 2 | const app = getApp() 3 | 4 | Page({ 5 | 6 | /** 7 | * 页面的初始数据 8 | */ 9 | data: { 10 | userInfo: null, 11 | }, 12 | 13 | onTapLogin() { 14 | app.login({ 15 | success: ({ userInfo }) => { 16 | this.setData({ 17 | userInfo 18 | }) 19 | } 20 | }) 21 | }, 22 | 23 | onTapAddress() { 24 | wx.showToast({ 25 | icon: 'none', 26 | title: '此功能暂未开放' 27 | }) 28 | }, 29 | 30 | onTapKf() { 31 | wx.showToast({ 32 | icon: 'none', 33 | title: '此功能暂未开放' 34 | }) 35 | }, 36 | 37 | 38 | /** 39 | * 生命周期函数--监听页面加载 40 | */ 41 | onLoad: function (options) { 42 | 43 | }, 44 | 45 | /** 46 | * 生命周期函数--监听页面初次渲染完成 47 | */ 48 | onReady: function () { 49 | 50 | }, 51 | 52 | /** 53 | * 生命周期函数--监听页面显示 54 | */ 55 | onShow: function () { 56 | app.checkSession({ 57 | success: ({ userInfo }) => { 58 | this.setData({ 59 | userInfo 60 | }) 61 | } 62 | }) 63 | }, 64 | 65 | /** 66 | * 生命周期函数--监听页面隐藏 67 | */ 68 | onHide: function () { 69 | 70 | }, 71 | 72 | /** 73 | * 生命周期函数--监听页面卸载 74 | */ 75 | onUnload: function () { 76 | 77 | }, 78 | 79 | /** 80 | * 页面相关事件处理函数--监听用户下拉动作 81 | */ 82 | onPullDownRefresh: function () { 83 | 84 | }, 85 | 86 | /** 87 | * 页面上拉触底事件的处理函数 88 | */ 89 | onReachBottom: function () { 90 | 91 | }, 92 | 93 | /** 94 | * 用户点击右上角分享 95 | */ 96 | onShareAppMessage: function () { 97 | 98 | } 99 | }) -------------------------------------------------------------------------------- /client/pages/user/user.json: -------------------------------------------------------------------------------- 1 | {"navigationBarTitleText": "个人中心"} -------------------------------------------------------------------------------- /client/pages/user/user.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 未登录 8 | 点击微信登录后可方便购物 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {{userInfo.nickName}} 17 | 18 | 19 | 20 | 21 | 收货地址 22 | 23 | 24 | 25 | 联系客服 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /client/pages/user/user.wxss: -------------------------------------------------------------------------------- 1 | /* pages/user/user.wxss */ 2 | 3 | .user-card { 4 | margin: 50rpx 27rpx 0; 5 | background: #FFFFFF; 6 | box-shadow: 0 2rpx 13rpx 5rpx rgba(0, 0, 0, 0.02); 7 | border-radius: 13rpx; 8 | } 9 | 10 | .user-info { 11 | display: flex; 12 | align-items: center; 13 | height: 200rpx; 14 | } 15 | 16 | .user-head { 17 | flex-shrink: 0; 18 | margin-left: 53rpx; 19 | height: 100rpx; 20 | width: 100rpx; 21 | background: #F5E069; 22 | border-radius: 50%; 23 | } 24 | 25 | .user-name { 26 | flex: 1; 27 | margin: 0 31rpx; 28 | font-weight: bold; 29 | white-space: nowrap; 30 | overflow: hidden; 31 | text-overflow: ellipsis; 32 | } 33 | 34 | .user-split { 35 | height: 8rpx; 36 | background: #F9F9F9; 37 | } 38 | 39 | .user-options .option { 40 | display: flex; 41 | align-items: center; 42 | margin-left: 46rpx; 43 | margin-right: 27rpx; 44 | height: 128rpx; 45 | border-bottom: 1px solid rgba(151, 151, 151, 0.2); 46 | } 47 | 48 | .user-options .option:last-child { 49 | border-bottom: none; 50 | } 51 | 52 | .user-options .option-title { 53 | flex: 1; 54 | font-size: 30rpx; 55 | color: rgba(29, 29, 38, 0.8); 56 | } 57 | 58 | .user-options .option-arrow { 59 | width: 11rpx; 60 | height: 18rpx; 61 | } -------------------------------------------------------------------------------- /client/utils/util.js: -------------------------------------------------------------------------------- 1 | const formatTime = date => { 2 | const year = date.getFullYear() 3 | const month = date.getMonth() + 1 4 | const day = date.getDate() 5 | const hour = date.getHours() 6 | const minute = date.getMinutes() 7 | const second = date.getSeconds() 8 | 9 | return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':') 10 | } 11 | 12 | const formatNumber = n => { 13 | n = n.toString() 14 | return n[1] ? n : '0' + n 15 | } 16 | 17 | 18 | // 显示繁忙提示 19 | var showBusy = text => wx.showToast({ 20 | title: text, 21 | icon: 'loading', 22 | duration: 10000 23 | }) 24 | 25 | // 显示成功提示 26 | var showSuccess = text => wx.showToast({ 27 | title: text, 28 | icon: 'success' 29 | }) 30 | 31 | // 显示失败提示 32 | var showModel = (title, content) => { 33 | wx.hideToast(); 34 | 35 | wx.showModal({ 36 | title, 37 | content: JSON.stringify(content), 38 | showCancel: false 39 | }) 40 | } 41 | 42 | module.exports = { formatTime, showBusy, showSuccess, showModel } 43 | -------------------------------------------------------------------------------- /client/vendor/wafer2-client-sdk/LICENSE: -------------------------------------------------------------------------------- 1 | LICENSE - "MIT License" 2 | 3 | Copyright (c) 2016 by Tencent Cloud 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /client/vendor/wafer2-client-sdk/README.md: -------------------------------------------------------------------------------- 1 | # 微信小程序客户端腾讯云增强 SDK 2 | 3 | [![Build Status](https://travis-ci.org/tencentyun/wafer-client-sdk.svg?branch=master)](https://travis-ci.org/tencentyun/wafer-client-sdk) 4 | [![Coverage Status](https://coveralls.io/repos/github/tencentyun/wafer-client-sdk/badge.svg?branch=master)](https://coveralls.io/github/tencentyun/wafer-client-sdk?branch=master) 5 | [![License](https://img.shields.io/github/license/tencentyun/wafer-client-sdk.svg)](LICENSE) 6 | 7 | 本 项目是 [Wafer](https://github.com/tencentyun/wafer-solution) 的组成部分,为小程序客户端开发提供 SDK 支持会话服务和信道服务。 8 | 9 | ## SDK 获取与安装 10 | 11 | 解决方案[客户端 Demo](https://github.com/tencentyun/wafer-client-demo) 已经集成并使用最新版的 SDK,需要快速了解的可以从 Demo 开始。 12 | 13 | 如果需要单独开始,本 SDK 已经发布为 bower 模块,可以直接安装到小程序目录中。 14 | 15 | ```sh 16 | npm install -g bower 17 | bower install qcloud-weapp-client-sdk 18 | ``` 19 | 20 | 安装之后,就可以使用 `require` 引用 SDK 模块: 21 | 22 | ```js 23 | var qcloud = require('./bower_components/qcloud-weapp-client-sdk/index.js'); 24 | ``` 25 | 26 | ## 会话服务 27 | 28 | [会话服务](https://github.com/tencentyun/wafer-solution/wiki/%E4%BC%9A%E8%AF%9D%E6%9C%8D%E5%8A%A1)让小程序拥有会话管理能力。 29 | 30 | ### 登录 31 | 32 | 登录可以在小程序和服务器之间建立会话,服务器由此可以获取到用户的标识和信息。 33 | 34 | ```js 35 | var qcloud = require('./bower_components/qcloud-weapp-client-sdk/index.js'); 36 | 37 | // 设置登录地址 38 | qcloud.setLoginUrl('https://199447.qcloud.la/login'); 39 | qcloud.login({ 40 | success: function (userInfo) { 41 | console.log('登录成功', userInfo); 42 | }, 43 | fail: function (err) { 44 | console.log('登录失败', err); 45 | } 46 | }); 47 | ``` 48 | 本 SDK 需要配合云端 SDK 才能提供完整会话服务。通过 [setLoginUrl](#setLoginUrl) 设置登录地址,云服务器在该地址上使用云端 SDK 处理登录请求。 49 | 50 | > `setLoginUrl` 方法设置登录地址之后会一直有效,因此你可以在微信小程序启动时设置。 51 | 52 | 登录成功后,可以获取到当前微信用户的基本信息。 53 | 54 | ### 请求 55 | 56 | 如果希望小程序的网络请求包含会话,登录之后使用 [request](#request) 方法进行网络请求即可。 57 | 58 | ```js 59 | qcloud.request({ 60 | url: 'http://199447.qcloud.la/user', 61 | success: function (response) { 62 | console.log(response); 63 | }, 64 | fail: function (err) { 65 | console.log(err); 66 | } 67 | }); 68 | ``` 69 | 70 | 如果调用 `request` 之前还没有登录,则请求不会带有会话。`request` 方法也支持 `login` 参数支持在请求之前自动登录。 71 | 72 | ```js 73 | // 使用 login 参数之前,需要设置登录地址 74 | qcloud.setLoginUrl('https://199447.qcloud.la/login'); 75 | qcloud.request({ 76 | login: true, 77 | url: 'http://199447.qcloud.la/user', 78 | success: function (response) { 79 | console.log(response); 80 | }, 81 | fail: function (err) { 82 | console.log(err); 83 | } 84 | }); 85 | ``` 86 | 87 | 关于会话服务详细技术说明,请参考 [Wiki](https://github.com/tencentyun/wafer-solution/wiki/%E4%BC%9A%E8%AF%9D%E6%9C%8D%E5%8A%A1)。 88 | 89 | ## 信道服务 90 | 91 | [信道服务](https://github.com/tencentyun/wafer-solution/wiki/%E4%BF%A1%E9%81%93%E6%9C%8D%E5%8A%A1)小程序支持利用腾讯云的信道资源使用 WebSocket 服务。 92 | 93 | ```js 94 | // 创建信道,需要给定后台服务地址 95 | var tunnel = this.tunnel = new qcloud.Tunnel('https://199447.qcloud.la/tunnel'); 96 | 97 | // 监听信道内置消息,包括 connect/close/reconnecting/reconnect/error 98 | tunnel.on('connect', () => console.log('WebSocket 信道已连接')); 99 | tunnel.on('close', () => console.log('WebSocket 信道已断开')); 100 | tunnel.on('reconnecting', () => console.log('WebSocket 信道正在重连...')); 101 | tunnel.on('reconnect', () => console.log('WebSocket 信道重连成功')); 102 | tunnel.on('error', error => console.error('信道发生错误:', error)); 103 | 104 | // 监听自定义消息(服务器进行推送) 105 | tunnel.on('speak', speak => console.log('收到 speak 消息:', speak)); 106 | 107 | // 打开信道 108 | tunnel.open(); 109 | // 发送消息 110 | tunnel.emit('speak', { word: "hello", who: { nickName: "techird" }}); 111 | // 关闭信道 112 | tunnel.close(); 113 | ``` 114 | 115 | 信道服务同样需要业务服务器配合云端 SDK 支持,构造信道实例的时候需要提供业务服务器提供的信道服务地址。通过监听信道消息以及自定义消息来通过信道实现业务。 116 | 117 | 关于信道使用的更完整实例,建议参考客户端 Demo 中的[三木聊天室应用源码](https://github.com/tencentyun/wafer-client-demo/blob/master/pages/chat/chat.js)。 118 | 119 | 关于信道服务详细技术说明,请参考 [Wiki](https://github.com/tencentyun/wafer-solution/wiki/%E4%BF%A1%E9%81%93%E6%9C%8D%E5%8A%A1)。 120 | 121 | ## API 122 | 123 | 124 | ### setLoginUrl 125 | 设置会话服务登录地址。 126 | 127 | #### 语法 128 | ```js 129 | qcloud.setLoginUrl(loginUrl); 130 | ``` 131 | 132 | #### 参数 133 | |参数         |类型 |说明 134 | |-------------|---------------|-------------- 135 | |loginUrl |string |会话服务登录地址 136 | 137 | ### login 138 | 登录,建立微信小程序会话。 139 | 140 | #### 语法 141 | ```js 142 | qcloud.login(options); 143 | ``` 144 | 145 | #### 参数 146 | |参数         |类型 |说明 147 | |-------------|---------------|-------------- 148 | |options     |PlainObject   |会话服务登录地址 149 | |options.success | () => void | 登录成功的回调 150 | |options.error | (error) => void | 登录失败的回调 151 | 152 | 153 | ### request 154 | 进行带会话的请求。 155 | 156 | #### 语法 157 | ```js 158 | qcloud.request(options); 159 | ``` 160 | 161 | #### 参数 162 | |参数         |类型 |说明 163 | |-------------|---------------|-------------- 164 | |options     |PlainObject   | 会话服务登录地址 165 | |options.login | bool         | 是否自动登录以获取会话,默认为 false 166 | |options.url   | string       | 必填,要请求的地址 167 | |options.header | PlainObject | 请求头设置,不允许设置 Referer 168 | |options.method | string     | 请求的方法,默认为 GET 169 | |options.success | (response) => void | 登录成功的回调。 170 | |options.error | (error) => void | 登录失败的回调 171 | |options.complete | () => void | 登录完成后回调,无论成功还是失败 172 | 173 | ### Tunnel 174 | 175 | 表示一个信道。由于小程序的限制,同一时间只能有一个打开的信道。 176 | 177 | #### constructor 178 | 179 | ##### 语法 180 | ```js 181 | var tunnel = new Tunnel(tunnelUrl); 182 | ``` 183 | 184 | #### 参数 185 | |参数         |类型 |说明 186 | |-------------|---------------|-------------- 187 | |tunnelUrl   |String   | 会话服务登录地址 188 | 189 | 190 | #### on 191 | 监听信道上的事件。信道上事件包括系统事件和服务器推送消息。 192 | 193 | ##### 语法 194 | ```js 195 | tunnel.on(type, listener); 196 | ``` 197 | 198 | ##### 参数 199 | |参数         |类型 |说明 200 | |-------------|---------------|-------------- 201 | |type       |string     | 监听的事件类型 202 | |listener     |(message?: any) => void | 监听器,具体类型的事件发生时调用监听器。如果是消息,则会有消息内容。 203 | 204 | ##### 事件 205 | |事件 |说明 206 | |-------------|------------------------------- 207 | |connect |信道连接成功后回调 208 | |close |信道关闭后回调 209 | |reconnecting |信道发生重连时回调 210 | |reconnected |信道重连成功后回调 211 | |error |信道发生错误后回调 212 | |[message]   |信道服务器推送过来的消息类型,如果消息类型和上面内置的时间类型冲突,需要在监听的时候在消息类型前加 `@` 213 | |\*           |监听所有事件和消息,监听器第一个参数接收到时间或消息类型  214 | 215 | #### open 216 | 打开信道,建立连接。由于小程序的限制,同一时间只能有一个打开的信道。 217 | 218 | ##### 语法 219 | ```js 220 | tunnel.open(); 221 | ``` 222 | 223 | #### emit 224 | 向信道推送消息。 225 | 226 | ##### 语法 227 | ```js 228 | tunnel.emit(type, content); 229 | ``` 230 | 231 | ##### 参数 232 | |参数         |类型 |说明 233 | |-------------|---------------|-------------- 234 | |type       |string       | 要推送的消息的类型 235 | |content |any | 要推送的消息的内容 236 | 237 | #### close 238 | 关闭信道 239 | 240 | ##### 语法 241 | ```js 242 | tunnel.close(); 243 | ``` 244 | 245 | ## LICENSE 246 | 247 | [MIT](LICENSE) 248 | -------------------------------------------------------------------------------- /client/vendor/wafer2-client-sdk/index.js: -------------------------------------------------------------------------------- 1 | var constants = require('./lib/constants'); 2 | var login = require('./lib/login'); 3 | var Session = require('./lib/session'); 4 | var request = require('./lib/request'); 5 | var Tunnel = require('./lib/tunnel'); 6 | 7 | var exports = module.exports = { 8 | login: login.login, 9 | setLoginUrl: login.setLoginUrl, 10 | LoginError: login.LoginError, 11 | 12 | clearSession: Session.clear, 13 | 14 | request: request.request, 15 | RequestError: request.RequestError, 16 | 17 | Tunnel: Tunnel, 18 | }; 19 | 20 | // 导出错误类型码 21 | Object.keys(constants).forEach(function (key) { 22 | if (key.indexOf('ERR_') === 0) { 23 | exports[key] = constants[key]; 24 | } 25 | }); -------------------------------------------------------------------------------- /client/vendor/wafer2-client-sdk/lib/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | WX_HEADER_CODE: 'X-WX-Code', 3 | WX_HEADER_ENCRYPTED_DATA: 'X-WX-Encrypted-Data', 4 | WX_HEADER_IV: 'X-WX-IV', 5 | WX_HEADER_ID: 'X-WX-Id', 6 | WX_HEADER_SKEY: 'X-WX-Skey', 7 | 8 | WX_SESSION_MAGIC_ID: 'F2C224D4-2BCE-4C64-AF9F-A6D872000D1A', 9 | 10 | ERR_INVALID_PARAMS: 'ERR_INVALID_PARAMS', 11 | 12 | ERR_WX_LOGIN_FAILED: 'ERR_WX_LOGIN_FAILED', 13 | ERR_WX_GET_USER_INFO: 'ERR_WX_GET_USER_INFO', 14 | ERR_LOGIN_TIMEOUT: 'ERR_LOGIN_TIMEOUT', 15 | ERR_LOGIN_FAILED: 'ERR_LOGIN_FAILED', 16 | ERR_LOGIN_SESSION_NOT_RECEIVED: 'ERR_LOGIN_MISSING_SESSION', 17 | 18 | ERR_SESSION_INVALID: 'ERR_SESSION_INVALID', 19 | ERR_CHECK_LOGIN_FAILED: 'ERR_CHECK_LOGIN_FAILED', 20 | }; -------------------------------------------------------------------------------- /client/vendor/wafer2-client-sdk/lib/login.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils'); 2 | var constants = require('./constants'); 3 | var Session = require('./session'); 4 | 5 | /*** 6 | * @class 7 | * 表示登录过程中发生的异常 8 | */ 9 | var LoginError = (function () { 10 | function LoginError(type, message) { 11 | Error.call(this, message); 12 | this.type = type; 13 | this.message = message; 14 | } 15 | 16 | LoginError.prototype = new Error(); 17 | LoginError.prototype.constructor = LoginError; 18 | 19 | return LoginError; 20 | })(); 21 | 22 | /** 23 | * 微信登录,获取 code 和 encryptData 24 | */ 25 | var getWxLoginResult = function getLoginCode(callback) { 26 | wx.login({ 27 | success: function (loginResult) { 28 | wx.getUserInfo({ 29 | success: function (userResult) { 30 | callback(null, { 31 | code: loginResult.code, 32 | encryptedData: userResult.encryptedData, 33 | iv: userResult.iv, 34 | userInfo: userResult.userInfo, 35 | }); 36 | }, 37 | 38 | fail: function (userError) { 39 | var error = new LoginError(constants.ERR_WX_GET_USER_INFO, '获取微信用户信息失败,请检查网络状态'); 40 | error.detail = userError; 41 | callback(error, null); 42 | }, 43 | }); 44 | }, 45 | 46 | fail: function (loginError) { 47 | var error = new LoginError(constants.ERR_WX_LOGIN_FAILED, '微信登录失败,请检查网络状态'); 48 | error.detail = loginError; 49 | callback(error, null); 50 | }, 51 | }); 52 | }; 53 | 54 | var noop = function noop() {}; 55 | var defaultOptions = { 56 | method: 'GET', 57 | success: noop, 58 | fail: noop, 59 | loginUrl: null, 60 | }; 61 | 62 | /** 63 | * @method 64 | * 进行服务器登录,以获得登录会话 65 | * 66 | * @param {Object} options 登录配置 67 | * @param {string} options.loginUrl 登录使用的 URL,服务器应该在这个 URL 上处理登录请求 68 | * @param {string} [options.method] 请求使用的 HTTP 方法,默认为 "GET" 69 | * @param {Function} options.success(userInfo) 登录成功后的回调函数,参数 userInfo 微信用户信息 70 | * @param {Function} options.fail(error) 登录失败后的回调函数,参数 error 错误信息 71 | */ 72 | var login = function login(options) { 73 | options = utils.extend({}, defaultOptions, options); 74 | 75 | if (!defaultOptions.loginUrl) { 76 | options.fail(new LoginError(constants.ERR_INVALID_PARAMS, '登录错误:缺少登录地址,请通过 setLoginUrl() 方法设置登录地址')); 77 | return; 78 | } 79 | 80 | var doLogin = () => getWxLoginResult(function (wxLoginError, wxLoginResult) { 81 | if (wxLoginError) { 82 | options.fail(wxLoginError); 83 | return; 84 | } 85 | 86 | var userInfo = wxLoginResult.userInfo; 87 | 88 | // 构造请求头,包含 code、encryptedData 和 iv 89 | var code = wxLoginResult.code; 90 | var encryptedData = wxLoginResult.encryptedData; 91 | var iv = wxLoginResult.iv; 92 | var header = {}; 93 | 94 | header[constants.WX_HEADER_CODE] = code; 95 | header[constants.WX_HEADER_ENCRYPTED_DATA] = encryptedData; 96 | header[constants.WX_HEADER_IV] = iv; 97 | 98 | // 请求服务器登录地址,获得会话信息 99 | wx.request({ 100 | url: options.loginUrl, 101 | header: header, 102 | method: options.method, 103 | data: options.data, 104 | success: function (result) { 105 | var data = result.data; 106 | 107 | // 成功地响应会话信息 108 | if (data && data.code === 0 && data.data.skey) { 109 | var res = data.data 110 | if (res.userinfo) { 111 | Session.set(res.skey); 112 | options.success(userInfo); 113 | } else { 114 | var errorMessage = '登录失败(' + data.error + '):' + (data.message || '未知错误'); 115 | var noSessionError = new LoginError(constants.ERR_LOGIN_SESSION_NOT_RECEIVED, errorMessage); 116 | options.fail(noSessionError); 117 | } 118 | 119 | // 没有正确响应会话信息 120 | } else { 121 | var noSessionError = new LoginError(constants.ERR_LOGIN_SESSION_NOT_RECEIVED, JSON.stringify(data)); 122 | options.fail(noSessionError); 123 | } 124 | }, 125 | 126 | // 响应错误 127 | fail: function (loginResponseError) { 128 | var error = new LoginError(constants.ERR_LOGIN_FAILED, '登录失败,可能是网络错误或者服务器发生异常'); 129 | options.fail(error); 130 | }, 131 | }); 132 | }); 133 | 134 | var session = Session.get(); 135 | if (session) { 136 | wx.checkSession({ 137 | success: function () { 138 | options.success(session.userInfo); 139 | }, 140 | 141 | fail: function () { 142 | Session.clear(); 143 | doLogin(); 144 | }, 145 | }); 146 | } else { 147 | doLogin(); 148 | } 149 | }; 150 | 151 | var setLoginUrl = function (loginUrl) { 152 | defaultOptions.loginUrl = loginUrl; 153 | }; 154 | 155 | module.exports = { 156 | LoginError: LoginError, 157 | login: login, 158 | setLoginUrl: setLoginUrl, 159 | }; -------------------------------------------------------------------------------- /client/vendor/wafer2-client-sdk/lib/request.js: -------------------------------------------------------------------------------- 1 | var constants = require('./constants'); 2 | var utils = require('./utils'); 3 | var Session = require('./session'); 4 | var loginLib = require('./login'); 5 | 6 | var noop = function noop() {}; 7 | 8 | var buildAuthHeader = function buildAuthHeader(session) { 9 | var header = {}; 10 | 11 | if (session) { 12 | header[constants.WX_HEADER_SKEY] = session; 13 | } 14 | 15 | return header; 16 | }; 17 | 18 | /*** 19 | * @class 20 | * 表示请求过程中发生的异常 21 | */ 22 | var RequestError = (function () { 23 | function RequestError(type, message) { 24 | Error.call(this, message); 25 | this.type = type; 26 | this.message = message; 27 | } 28 | 29 | RequestError.prototype = new Error(); 30 | RequestError.prototype.constructor = RequestError; 31 | 32 | return RequestError; 33 | })(); 34 | 35 | function request(options) { 36 | if (typeof options !== 'object') { 37 | var message = '请求传参应为 object 类型,但实际传了 ' + (typeof options) + ' 类型'; 38 | throw new RequestError(constants.ERR_INVALID_PARAMS, message); 39 | } 40 | 41 | var requireLogin = options.login; 42 | var success = options.success || noop; 43 | var fail = options.fail || noop; 44 | var complete = options.complete || noop; 45 | var originHeader = options.header || {}; 46 | 47 | // 成功回调 48 | var callSuccess = function () { 49 | success.apply(null, arguments); 50 | complete.apply(null, arguments); 51 | }; 52 | 53 | // 失败回调 54 | var callFail = function (error) { 55 | fail.call(null, error); 56 | complete.call(null, error); 57 | }; 58 | 59 | // 是否已经进行过重试 60 | var hasRetried = false; 61 | 62 | if (requireLogin) { 63 | doRequestWithLogin(); 64 | } else { 65 | doRequest(); 66 | } 67 | 68 | // 登录后再请求 69 | function doRequestWithLogin() { 70 | loginLib.login({ success: doRequest, fail: callFail }); 71 | } 72 | 73 | // 实际进行请求的方法 74 | function doRequest() { 75 | var authHeader = buildAuthHeader(Session.get()); 76 | 77 | wx.request(utils.extend({}, options, { 78 | header: utils.extend({}, originHeader, authHeader), 79 | 80 | success: function (response) { 81 | var data = response.data; 82 | 83 | var error, message; 84 | if (data && data.code === -1) { 85 | Session.clear(); 86 | // 如果是登录态无效,并且还没重试过,会尝试登录后刷新凭据重新请求 87 | if (!hasRetried) { 88 | hasRetried = true; 89 | doRequestWithLogin(); 90 | return; 91 | } 92 | 93 | message = '登录态已过期'; 94 | error = new RequestError(data.error, message); 95 | 96 | callFail(error); 97 | return; 98 | } else { 99 | callSuccess.apply(null, arguments); 100 | } 101 | }, 102 | 103 | fail: callFail, 104 | complete: noop, 105 | })); 106 | }; 107 | 108 | }; 109 | 110 | module.exports = { 111 | RequestError: RequestError, 112 | request: request, 113 | }; -------------------------------------------------------------------------------- /client/vendor/wafer2-client-sdk/lib/session.js: -------------------------------------------------------------------------------- 1 | var constants = require('./constants'); 2 | var SESSION_KEY = 'weapp_session_' + constants.WX_SESSION_MAGIC_ID; 3 | 4 | var Session = { 5 | get: function () { 6 | return wx.getStorageSync(SESSION_KEY) || null; 7 | }, 8 | 9 | set: function (session) { 10 | wx.setStorageSync(SESSION_KEY, session); 11 | }, 12 | 13 | clear: function () { 14 | wx.removeStorageSync(SESSION_KEY); 15 | }, 16 | }; 17 | 18 | module.exports = Session; -------------------------------------------------------------------------------- /client/vendor/wafer2-client-sdk/lib/tunnel.js: -------------------------------------------------------------------------------- 1 | var requestLib = require('./request'); 2 | var wxTunnel = require('./wxTunnel'); 3 | 4 | /** 5 | * 当前打开的信道,同一时间只能有一个信道打开 6 | */ 7 | var currentTunnel = null; 8 | 9 | // 信道状态枚举 10 | var STATUS_CLOSED = Tunnel.STATUS_CLOSED = 'CLOSED'; 11 | var STATUS_CONNECTING = Tunnel.STATUS_CONNECTING = 'CONNECTING'; 12 | var STATUS_ACTIVE = Tunnel.STATUS_ACTIVE = 'ACTIVE'; 13 | var STATUS_RECONNECTING = Tunnel.STATUS_RECONNECTING = 'RECONNECTING'; 14 | 15 | // 错误类型枚举 16 | var ERR_CONNECT_SERVICE = Tunnel.ERR_CONNECT_SERVICE = 1001; 17 | var ERR_CONNECT_SOCKET = Tunnel.ERR_CONNECT_SOCKET = 1002; 18 | var ERR_RECONNECT = Tunnel.ERR_RECONNECT = 2001; 19 | var ERR_SOCKET_ERROR = Tunnel.ERR_SOCKET_ERROR = 3001; 20 | 21 | // 包类型枚举 22 | var PACKET_TYPE_MESSAGE = 'message'; 23 | var PACKET_TYPE_PING = 'ping'; 24 | var PACKET_TYPE_PONG = 'pong'; 25 | var PACKET_TYPE_TIMEOUT = 'timeout'; 26 | var PACKET_TYPE_CLOSE = 'close'; 27 | 28 | // 断线重连最多尝试 5 次 29 | var DEFAULT_MAX_RECONNECT_TRY_TIMES = 5; 30 | 31 | // 每次重连前,等待时间的增量值 32 | var DEFAULT_RECONNECT_TIME_INCREASE = 1000; 33 | 34 | function Tunnel(serviceUrl) { 35 | if (currentTunnel && currentTunnel.status !== STATUS_CLOSED) { 36 | throw new Error('当前有未关闭的信道,请先关闭之前的信道,再打开新信道'); 37 | } 38 | 39 | currentTunnel = this; 40 | 41 | // 等确认微信小程序全面支持 ES6 就不用那么麻烦了 42 | var me = this; 43 | 44 | //========================================================================= 45 | // 暴露实例状态以及方法 46 | //========================================================================= 47 | this.serviceUrl = serviceUrl; 48 | this.socketUrl = null; 49 | this.status = null; 50 | 51 | this.open = openConnect; 52 | this.on = registerEventHandler; 53 | this.emit = emitMessagePacket; 54 | this.close = close; 55 | 56 | this.isClosed = isClosed; 57 | this.isConnecting = isConnecting; 58 | this.isActive = isActive; 59 | this.isReconnecting = isReconnecting; 60 | 61 | 62 | //========================================================================= 63 | // 信道状态处理,状态说明: 64 | // closed - 已关闭 65 | // connecting - 首次连接 66 | // active - 当前信道已经在工作 67 | // reconnecting - 断线重连中 68 | //========================================================================= 69 | function isClosed() { return me.status === STATUS_CLOSED; } 70 | function isConnecting() { return me.status === STATUS_CONNECTING; } 71 | function isActive() { return me.status === STATUS_ACTIVE; } 72 | function isReconnecting() { return me.status === STATUS_RECONNECTING; } 73 | 74 | function setStatus(status) { 75 | var lastStatus = me.status; 76 | if (lastStatus !== status) { 77 | me.status = status; 78 | } 79 | } 80 | 81 | // 初始为关闭状态 82 | setStatus(STATUS_CLOSED); 83 | 84 | 85 | //========================================================================= 86 | // 信道事件处理机制 87 | // 信道事件包括: 88 | // connect - 连接已建立 89 | // close - 连接被关闭(包括主动关闭和被动关闭) 90 | // reconnecting - 开始重连 91 | // reconnect - 重连成功 92 | // error - 发生错误,其中包括连接失败、重连失败、解包失败等等 93 | // [message] - 信道服务器发送过来的其它事件类型,如果事件类型和上面内置的事件类型冲突,将在事件类型前面添加前缀 `@` 94 | //========================================================================= 95 | var preservedEventTypes = 'connect,close,reconnecting,reconnect,error'.split(','); 96 | var eventHandlers = []; 97 | 98 | /** 99 | * 注册消息处理函数 100 | * @param {string} messageType 支持内置消息类型("connect"|"close"|"reconnecting"|"reconnect"|"error")以及业务消息类型 101 | */ 102 | function registerEventHandler(eventType, eventHandler) { 103 | if (typeof eventHandler === 'function') { 104 | eventHandlers.push([eventType, eventHandler]); 105 | } 106 | } 107 | 108 | /** 109 | * 派发事件,通知所有处理函数进行处理 110 | */ 111 | function dispatchEvent(eventType, eventPayload) { 112 | eventHandlers.forEach(function (handler) { 113 | var handleType = handler[0]; 114 | var handleFn = handler[1]; 115 | 116 | if (handleType === '*') { 117 | handleFn(eventType, eventPayload); 118 | } else if (handleType === eventType) { 119 | handleFn(eventPayload); 120 | } 121 | }); 122 | } 123 | 124 | /** 125 | * 派发事件,事件类型和系统保留冲突的,事件名会自动加上 '@' 前缀 126 | */ 127 | function dispatchEscapedEvent(eventType, eventPayload) { 128 | if (preservedEventTypes.indexOf(eventType) > -1) { 129 | eventType = '@' + eventType; 130 | } 131 | 132 | dispatchEvent(eventType, eventPayload); 133 | } 134 | 135 | 136 | //========================================================================= 137 | // 信道连接控制 138 | //========================================================================= 139 | var isFirstConnection = true; 140 | var isOpening = false; 141 | 142 | /** 143 | * 连接信道服务器,获取 WebSocket 连接地址,获取地址成功后,开始进行 WebSocket 连接 144 | */ 145 | function openConnect() { 146 | if (isOpening) return; 147 | isOpening = true; 148 | 149 | // 只有关闭状态才会重新进入准备中 150 | setStatus(isFirstConnection ? STATUS_CONNECTING : STATUS_RECONNECTING); 151 | 152 | requestLib.request({ 153 | url: serviceUrl, 154 | method: 'GET', 155 | success: function (response) { 156 | if (+response.statusCode === 200 && response.data && response.data.data.connectUrl) { 157 | openSocket(me.socketUrl = response.data.data.connectUrl); 158 | } else { 159 | dispatchConnectServiceError(response); 160 | } 161 | }, 162 | fail: dispatchConnectServiceError, 163 | complete: () => isOpening = false, 164 | }); 165 | 166 | function dispatchConnectServiceError(detail) { 167 | if (isFirstConnection) { 168 | setStatus(STATUS_CLOSED); 169 | 170 | dispatchEvent('error', { 171 | code: ERR_CONNECT_SERVICE, 172 | message: '连接信道服务失败,网络错误或者信道服务没有正确响应', 173 | detail: detail || null, 174 | }); 175 | 176 | } else { 177 | startReconnect(detail); 178 | } 179 | } 180 | } 181 | 182 | /** 183 | * 打开 WebSocket 连接,打开后,注册微信的 Socket 处理方法 184 | */ 185 | function openSocket(url) { 186 | wxTunnel.listen({ 187 | onOpen: handleSocketOpen, 188 | onMessage: handleSocketMessage, 189 | onClose: handleSocketClose, 190 | onError: handleSocketError, 191 | }); 192 | 193 | wx.connectSocket({ url: url }); 194 | isFirstConnection = false; 195 | } 196 | 197 | 198 | //========================================================================= 199 | // 处理消息通讯 200 | // 201 | // packet - 数据包,序列化形式为 `${type}` 或者 `${type}:${content}` 202 | // packet.type - 包类型,包括 message, ping, pong, close 203 | // packet.content? - 当包类型为 message 的时候,会附带 message 数据 204 | // 205 | // message - 消息体,会使用 JSON 序列化后作为 packet.content 206 | // message.type - 消息类型,表示业务消息类型 207 | // message.content? - 消息实体,可以为任意类型,表示消息的附带数据,也可以为空 208 | // 209 | // 数据包示例: 210 | // - 'ping' 表示 Ping 数据包 211 | // - 'message:{"type":"speak","content":"hello"}' 表示一个打招呼的数据包 212 | //========================================================================= 213 | 214 | // 连接还没成功建立的时候,需要发送的包会先存放到队列里 215 | var queuedPackets = []; 216 | 217 | /** 218 | * WebSocket 打开之后,更新状态,同时发送所有遗留的数据包 219 | */ 220 | function handleSocketOpen() { 221 | /* istanbul ignore else */ 222 | if (isConnecting()) { 223 | dispatchEvent('connect'); 224 | 225 | } 226 | else if (isReconnecting()) { 227 | dispatchEvent('reconnect'); 228 | resetReconnectionContext(); 229 | } 230 | 231 | setStatus(STATUS_ACTIVE); 232 | emitQueuedPackets(); 233 | nextPing(); 234 | } 235 | 236 | /** 237 | * 收到 WebSocket 数据包,交给处理函数 238 | */ 239 | function handleSocketMessage(message) { 240 | resolvePacket(message.data); 241 | } 242 | 243 | /** 244 | * 发送数据包,如果信道没有激活,将先存放队列 245 | */ 246 | function emitPacket(packet) { 247 | if (isActive()) { 248 | sendPacket(packet); 249 | } else { 250 | queuedPackets.push(packet); 251 | } 252 | } 253 | 254 | /** 255 | * 数据包推送到信道 256 | */ 257 | function sendPacket(packet) { 258 | var encodedPacket = [packet.type]; 259 | 260 | if (packet.content) { 261 | encodedPacket.push(JSON.stringify(packet.content)); 262 | } 263 | 264 | wx.sendSocketMessage({ 265 | data: encodedPacket.join(':'), 266 | fail: handleSocketError, 267 | }); 268 | } 269 | 270 | function emitQueuedPackets() { 271 | queuedPackets.forEach(emitPacket); 272 | 273 | // empty queued packets 274 | queuedPackets.length = 0; 275 | } 276 | 277 | /** 278 | * 发送消息包 279 | */ 280 | function emitMessagePacket(messageType, messageContent) { 281 | var packet = { 282 | type: PACKET_TYPE_MESSAGE, 283 | content: { 284 | type: messageType, 285 | content: messageContent, 286 | }, 287 | }; 288 | 289 | emitPacket(packet); 290 | } 291 | 292 | /** 293 | * 发送 Ping 包 294 | */ 295 | function emitPingPacket() { 296 | emitPacket({ type: PACKET_TYPE_PING }); 297 | } 298 | 299 | /** 300 | * 发送关闭包 301 | */ 302 | function emitClosePacket() { 303 | emitPacket({ type: PACKET_TYPE_CLOSE }); 304 | } 305 | 306 | /** 307 | * 解析并处理从信道接收到的包 308 | */ 309 | function resolvePacket(raw) { 310 | var packetParts = raw.split(':'); 311 | var packetType = packetParts.shift(); 312 | var packetContent = packetParts.join(':') || null; 313 | var packet = { type: packetType }; 314 | 315 | if (packetContent) { 316 | try { 317 | packet.content = JSON.parse(packetContent); 318 | } catch (e) {} 319 | } 320 | 321 | switch (packet.type) { 322 | case PACKET_TYPE_MESSAGE: 323 | handleMessagePacket(packet); 324 | break; 325 | case PACKET_TYPE_PONG: 326 | handlePongPacket(packet); 327 | break; 328 | case PACKET_TYPE_TIMEOUT: 329 | handleTimeoutPacket(packet); 330 | break; 331 | case PACKET_TYPE_CLOSE: 332 | handleClosePacket(packet); 333 | break; 334 | default: 335 | handleUnknownPacket(packet); 336 | break; 337 | } 338 | } 339 | 340 | /** 341 | * 收到消息包,直接 dispatch 给处理函数 342 | */ 343 | function handleMessagePacket(packet) { 344 | var message = packet.content; 345 | dispatchEscapedEvent(message.type, message.content); 346 | } 347 | 348 | 349 | //========================================================================= 350 | // 心跳、断开与重连处理 351 | //========================================================================= 352 | 353 | /** 354 | * Ping-Pong 心跳检测超时控制,这个值有两个作用: 355 | * 1. 表示收到服务器的 Pong 相应之后,过多久再发下一次 Ping 356 | * 2. 如果 Ping 发送之后,超过这个时间还没收到 Pong,断开与服务器的连接 357 | * 该值将在与信道服务器建立连接后被更新 358 | */ 359 | let pingPongTimeout = 15000; 360 | let pingTimer = 0; 361 | let pongTimer = 0; 362 | 363 | /** 364 | * 信道服务器返回 Ping-Pong 控制超时时间 365 | */ 366 | function handleTimeoutPacket(packet) { 367 | var timeout = packet.content * 1000; 368 | /* istanbul ignore else */ 369 | if (!isNaN(timeout)) { 370 | pingPongTimeout = timeout; 371 | ping(); 372 | } 373 | } 374 | 375 | /** 376 | * 收到服务器 Pong 响应,定时发送下一个 Ping 377 | */ 378 | function handlePongPacket(packet) { 379 | nextPing(); 380 | } 381 | 382 | /** 383 | * 发送下一个 Ping 包 384 | */ 385 | function nextPing() { 386 | clearTimeout(pingTimer); 387 | clearTimeout(pongTimer); 388 | pingTimer = setTimeout(ping, pingPongTimeout); 389 | } 390 | 391 | /** 392 | * 发送 Ping,等待 Pong 393 | */ 394 | function ping() { 395 | /* istanbul ignore else */ 396 | if (isActive()) { 397 | emitPingPacket(); 398 | 399 | // 超时没有响应,关闭信道 400 | pongTimer = setTimeout(handlePongTimeout, pingPongTimeout); 401 | } 402 | } 403 | 404 | /** 405 | * Pong 超时没有响应,信道可能已经不可用,需要断开重连 406 | */ 407 | function handlePongTimeout() { 408 | startReconnect('服务器已失去响应'); 409 | } 410 | 411 | // 已经重连失败的次数 412 | var reconnectTryTimes = 0; 413 | 414 | // 最多允许失败次数 415 | var maxReconnectTryTimes = Tunnel.MAX_RECONNECT_TRY_TIMES || DEFAULT_MAX_RECONNECT_TRY_TIMES; 416 | 417 | // 重连前等待的时间 418 | var waitBeforeReconnect = 0; 419 | 420 | // 重连前等待时间增量 421 | var reconnectTimeIncrease = Tunnel.RECONNECT_TIME_INCREASE || DEFAULT_RECONNECT_TIME_INCREASE; 422 | 423 | var reconnectTimer = 0; 424 | 425 | function startReconnect(lastError) { 426 | if (reconnectTryTimes >= maxReconnectTryTimes) { 427 | close(); 428 | 429 | dispatchEvent('error', { 430 | code: ERR_RECONNECT, 431 | message: '重连失败', 432 | detail: lastError, 433 | }); 434 | } 435 | else { 436 | wx.closeSocket(); 437 | waitBeforeReconnect += reconnectTimeIncrease; 438 | setStatus(STATUS_RECONNECTING); 439 | reconnectTimer = setTimeout(doReconnect, waitBeforeReconnect); 440 | } 441 | 442 | if (reconnectTryTimes === 0) { 443 | dispatchEvent('reconnecting'); 444 | } 445 | 446 | reconnectTryTimes += 1; 447 | } 448 | 449 | function doReconnect() { 450 | openConnect(); 451 | } 452 | 453 | function resetReconnectionContext() { 454 | reconnectTryTimes = 0; 455 | waitBeforeReconnect = 0; 456 | } 457 | 458 | /** 459 | * 收到服务器的关闭请求 460 | */ 461 | function handleClosePacket(packet) { 462 | close(); 463 | } 464 | 465 | function handleUnknownPacket(packet) { 466 | // throw away 467 | } 468 | 469 | var isClosing = false; 470 | 471 | /** 472 | * 收到 WebSocket 断开的消息,处理断开逻辑 473 | */ 474 | function handleSocketClose() { 475 | /* istanbul ignore if */ 476 | if (isClosing) return; 477 | 478 | /* istanbul ignore else */ 479 | if (isActive()) { 480 | // 意外断开的情况,进行重连 481 | startReconnect('链接已断开'); 482 | } 483 | } 484 | 485 | function close() { 486 | isClosing = true; 487 | closeSocket(); 488 | setStatus(STATUS_CLOSED); 489 | resetReconnectionContext(); 490 | isFirstConnection = false; 491 | clearTimeout(pingTimer); 492 | clearTimeout(pongTimer); 493 | clearTimeout(reconnectTimer); 494 | dispatchEvent('close'); 495 | isClosing = false; 496 | } 497 | 498 | function closeSocket(emitClose) { 499 | if (isActive() && emitClose !== false) { 500 | emitClosePacket(); 501 | } 502 | 503 | wx.closeSocket(); 504 | } 505 | 506 | 507 | //========================================================================= 508 | // 错误处理 509 | //========================================================================= 510 | 511 | /** 512 | * 错误处理 513 | */ 514 | function handleSocketError(detail) { 515 | switch (me.status) { 516 | case Tunnel.STATUS_CONNECTING: 517 | dispatchEvent('error', { 518 | code: ERR_SOCKET_ERROR, 519 | message: '连接信道失败,网络错误或者信道服务不可用', 520 | detail: detail, 521 | }); 522 | break; 523 | } 524 | } 525 | 526 | } 527 | 528 | module.exports = Tunnel; -------------------------------------------------------------------------------- /client/vendor/wafer2-client-sdk/lib/utils.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * 拓展对象 4 | */ 5 | exports.extend = function extend(target) { 6 | var sources = Array.prototype.slice.call(arguments, 1); 7 | 8 | for (var i = 0; i < sources.length; i += 1) { 9 | var source = sources[i]; 10 | for (var key in source) { 11 | if (source.hasOwnProperty(key)) { 12 | target[key] = source[key]; 13 | } 14 | } 15 | } 16 | 17 | return target; 18 | }; -------------------------------------------------------------------------------- /client/vendor/wafer2-client-sdk/lib/wxTunnel.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore next */ 2 | const noop = () => void(0); 3 | 4 | let onOpen, onClose, onMessage, onError; 5 | 6 | /* istanbul ignore next */ 7 | function listen(listener) { 8 | if (listener) { 9 | onOpen = listener.onOpen; 10 | onClose = listener.onClose; 11 | onMessage = listener.onMessage; 12 | onError = listener.onError; 13 | } else { 14 | onOpen = noop; 15 | onClose = noop; 16 | onMessage = noop; 17 | onError = noop; 18 | } 19 | } 20 | 21 | /* istanbul ignore next */ 22 | function bind() { 23 | wx.onSocketOpen(result => onOpen(result)); 24 | wx.onSocketClose(result => onClose(result)); 25 | wx.onSocketMessage(result => onMessage(result)); 26 | wx.onSocketError(error => onError(error)); 27 | } 28 | 29 | listen(null); 30 | bind(); 31 | 32 | module.exports = { listen }; -------------------------------------------------------------------------------- /client/vendor/wafer2-client-sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_from": "wafer2-client-sdk", 3 | "_id": "wafer2-client-sdk@1.0.0", 4 | "_inBundle": false, 5 | "_integrity": "sha1-4hExQwJ+2YIN3LOn0EtbBd8uTYg=", 6 | "_location": "/wafer2-client-sdk", 7 | "_phantomChildren": {}, 8 | "_requested": { 9 | "type": "tag", 10 | "registry": true, 11 | "raw": "wafer2-client-sdk", 12 | "name": "wafer2-client-sdk", 13 | "escapedName": "wafer2-client-sdk", 14 | "rawSpec": "", 15 | "saveSpec": null, 16 | "fetchSpec": "latest" 17 | }, 18 | "_requiredBy": [ 19 | "#USER", 20 | "/" 21 | ], 22 | "_resolved": "http://r.tnpm.oa.com/wafer2-client-sdk/download/wafer2-client-sdk-1.0.0.tgz", 23 | "_shasum": "e2113143027ed9820ddcb3a7d04b5b05df2e4d88", 24 | "_spec": "wafer2-client-sdk", 25 | "_where": "/Users/Jason/Tencent/ide-test/wafer-client-demo", 26 | "author": { 27 | "name": "CFETeam" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/tencentyun/wafer2-client-sdk/issues" 31 | }, 32 | "bundleDependencies": false, 33 | "deprecated": false, 34 | "description": "Wafer client SDK", 35 | "directories": { 36 | "lib": "lib" 37 | }, 38 | "homepage": "https://github.com/tencentyun/wafer2-client-sdk#readme", 39 | "license": "MIT", 40 | "main": "index.js", 41 | "name": "wafer2-client-sdk", 42 | "repository": { 43 | "type": "git", 44 | "url": "git+https://github.com/tencentyun/wafer2-client-sdk.git" 45 | }, 46 | "version": "1.0.0" 47 | } 48 | -------------------------------------------------------------------------------- /images&sql/demo.sql: -------------------------------------------------------------------------------- 1 | -- phpMyAdmin SQL Dump 2 | -- version 4.7.0 3 | -- https://www.phpmyadmin.net/ 4 | -- 5 | -- Host: localhost 6 | -- Generation Time: 2018-02-12 08:49:09 7 | -- 服务器版本: 5.7.18 8 | -- PHP Version: 5.6.30 9 | 10 | SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; 11 | SET AUTOCOMMIT = 0; 12 | START TRANSACTION; 13 | SET time_zone = "+00:00"; 14 | 15 | SET @IMAGE_BASE_URL = "YOUR_OWN_IMAGE_BASE_URL"; -- FOR EXAMPLE: https://*****.ap-shanghai.myqcloud.com/ 16 | 17 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 18 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 19 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 20 | /*!40101 SET NAMES utf8mb4 */; 21 | 22 | -- 23 | -- Database: `cAuth` 24 | -- 25 | 26 | -- -------------------------------------------------------- 27 | 28 | -- 29 | -- 表的结构 `comment` 30 | -- 31 | 32 | CREATE TABLE `comment` ( 33 | `id` int(11) NOT NULL, 34 | `user` varchar(255) NOT NULL, 35 | `username` varchar(255) DEFAULT NULL, 36 | `avatar` varchar(255) NOT NULL, 37 | `content` varchar(511) CHARACTER SET utf8 DEFAULT NULL, 38 | `images` varchar(1023) DEFAULT NULL, 39 | `product_id` int(11) NOT NULL, 40 | `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP 41 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1; 42 | 43 | -- -------------------------------------------------------- 44 | 45 | -- 46 | -- 表的结构 `order_product` 47 | -- 48 | 49 | CREATE TABLE `order_product` ( 50 | `order_id` int(11) NOT NULL, 51 | `product_id` int(11) NOT NULL, 52 | `count` int(11) NOT NULL DEFAULT '0' 53 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1; 54 | 55 | -- -------------------------------------------------------- 56 | 57 | -- 58 | -- 表的结构 `order_user` 59 | -- 60 | 61 | CREATE TABLE `order_user` ( 62 | `id` int(11) NOT NULL, 63 | `user` varchar(255) NOT NULL, 64 | `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP 65 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 66 | 67 | -- -------------------------------------------------------- 68 | 69 | -- 70 | -- 表的结构 `product` 71 | -- 72 | 73 | CREATE TABLE `product` ( 74 | `id` int(11) NOT NULL COMMENT 'id', 75 | `image` varchar(255) NOT NULL COMMENT '图片', 76 | `name` varchar(64) CHARACTER SET utf8 NOT NULL COMMENT '名称', 77 | `price` decimal(11,2) NOT NULL COMMENT '价格', 78 | `source` varchar(255) CHARACTER SET utf8 NOT NULL 79 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1; 80 | 81 | -- 82 | -- 转存表中的数据 `product` 83 | -- 84 | 85 | INSERT INTO `product` (`id`, `image`, `name`, `price`, `source`) VALUES 86 | (1, CONCAT(@IMAGE_BASE_URL, 'product1.jpg'), '钱包', '132.00', '国内·广东'), 87 | (2, CONCAT(@IMAGE_BASE_URL, 'product2.jpg'), '金色木吉他', '480.50', '国内·广东'), 88 | (3, CONCAT(@IMAGE_BASE_URL, 'product3.jpg'), '红纹铁质装订机', '28.00', '国内·福建'), 89 | (4, CONCAT(@IMAGE_BASE_URL, 'product4.jpg'), '新鲜有机青蔬', '30.90', '国内·江苏'), 90 | (5, CONCAT(@IMAGE_BASE_URL, 'product5.jpg'), '仿铁盘创意时钟', '45.00', '海外·瑞典'), 91 | (6, CONCAT(@IMAGE_BASE_URL, 'product6.jpg'), '新鲜采摘葡萄', '24.80', '国内·新疆'), 92 | (7, CONCAT(@IMAGE_BASE_URL, 'product7.jpg'), '果蔬大礼包', '158.00', '海外·新西兰'), 93 | (8, CONCAT(@IMAGE_BASE_URL, 'product8.jpg'), '红色复古轿车模型', '35.00', '海外·德国'), 94 | (9, CONCAT(@IMAGE_BASE_URL, 'product9.jpg'), '风驰电掣小摩托', '249.00', '国内·浙江'), 95 | (10, CONCAT(@IMAGE_BASE_URL, 'product10.jpg'), '筐装大红苹果', '29.80', '国内·山东'), 96 | (11, CONCAT(@IMAGE_BASE_URL, 'product11.jpg'), '精装耐用男鞋', '335.00', '国内·广东'), 97 | (12, CONCAT(@IMAGE_BASE_URL, 'product12.jpg'), '宗教圣地旅游纪念', '1668.00', '海外·印度'), 98 | (13, CONCAT(@IMAGE_BASE_URL, 'product13.jpg'), '高品质原装泵', '2000.80', '国内·河北'), 99 | (14, CONCAT(@IMAGE_BASE_URL, 'product14.jpg'), '金刚轱辘圈', '34.00', '国内·辽宁'), 100 | (15, CONCAT(@IMAGE_BASE_URL, 'product15.jpg'), '万圣节南瓜', '29.90', '海外·美国'); 101 | 102 | -- -------------------------------------------------------- 103 | 104 | -- 105 | -- 表的结构 `trolley_user` 106 | -- 107 | 108 | CREATE TABLE `trolley_user` ( 109 | `id` int(11) NOT NULL, 110 | `user` varchar(255) NOT NULL, 111 | `count` int(11) NOT NULL DEFAULT '0' 112 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 113 | 114 | -- 115 | -- Indexes for dumped tables 116 | -- 117 | 118 | -- 119 | -- Indexes for table `comment` 120 | -- 121 | ALTER TABLE `comment` 122 | ADD PRIMARY KEY (`id`), 123 | ADD KEY `product` (`product_id`); 124 | 125 | -- 126 | -- Indexes for table `order_product` 127 | -- 128 | ALTER TABLE `order_product` 129 | ADD PRIMARY KEY (`order_id`,`product_id`) USING BTREE, 130 | ADD KEY `product_link` (`product_id`); 131 | 132 | -- 133 | -- Indexes for table `order_user` 134 | -- 135 | ALTER TABLE `order_user` 136 | ADD PRIMARY KEY (`id`), 137 | ADD KEY `user-order` (`user`); 138 | 139 | -- 140 | -- Indexes for table `product` 141 | -- 142 | ALTER TABLE `product` 143 | ADD PRIMARY KEY (`id`); 144 | 145 | -- 146 | -- Indexes for table `trolley_user` 147 | -- 148 | ALTER TABLE `trolley_user` 149 | ADD PRIMARY KEY (`id`); 150 | 151 | -- 152 | -- 在导出的表使用AUTO_INCREMENT 153 | -- 154 | 155 | -- 156 | -- 使用表AUTO_INCREMENT `comment` 157 | -- 158 | ALTER TABLE `comment` 159 | MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4; 160 | -- 161 | -- 使用表AUTO_INCREMENT `order_user` 162 | -- 163 | ALTER TABLE `order_user` 164 | MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=11; 165 | -- 166 | -- 使用表AUTO_INCREMENT `product` 167 | -- 168 | ALTER TABLE `product` 169 | MODIFY `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id', AUTO_INCREMENT=19; 170 | -- 171 | -- 限制导出的表 172 | -- 173 | 174 | -- 175 | -- 限制表 `comment` 176 | -- 177 | ALTER TABLE `comment` 178 | ADD CONSTRAINT `comment_link` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`); 179 | 180 | -- 181 | -- 限制表 `order_product` 182 | -- 183 | ALTER TABLE `order_product` 184 | ADD CONSTRAINT `order_link` FOREIGN KEY (`order_id`) REFERENCES `order_user` (`id`), 185 | ADD CONSTRAINT `product_link` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`); 186 | 187 | -- 188 | -- 限制表 `trolley_user` 189 | -- 190 | ALTER TABLE `trolley_user` 191 | ADD CONSTRAINT `trolley_link` FOREIGN KEY (`id`) REFERENCES `product` (`id`); 192 | COMMIT; 193 | 194 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 195 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 196 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 197 | -------------------------------------------------------------------------------- /images&sql/products/product1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/images&sql/products/product1.jpg -------------------------------------------------------------------------------- /images&sql/products/product10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/images&sql/products/product10.jpg -------------------------------------------------------------------------------- /images&sql/products/product11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/images&sql/products/product11.jpg -------------------------------------------------------------------------------- /images&sql/products/product12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/images&sql/products/product12.jpg -------------------------------------------------------------------------------- /images&sql/products/product13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/images&sql/products/product13.jpg -------------------------------------------------------------------------------- /images&sql/products/product14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/images&sql/products/product14.jpg -------------------------------------------------------------------------------- /images&sql/products/product15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/images&sql/products/product15.jpg -------------------------------------------------------------------------------- /images&sql/products/product2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/images&sql/products/product2.jpg -------------------------------------------------------------------------------- /images&sql/products/product3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/images&sql/products/product3.jpg -------------------------------------------------------------------------------- /images&sql/products/product4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/images&sql/products/product4.jpg -------------------------------------------------------------------------------- /images&sql/products/product5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/images&sql/products/product5.jpg -------------------------------------------------------------------------------- /images&sql/products/product6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/images&sql/products/product6.jpg -------------------------------------------------------------------------------- /images&sql/products/product7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/images&sql/products/product7.jpg -------------------------------------------------------------------------------- /images&sql/products/product8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/images&sql/products/product8.jpg -------------------------------------------------------------------------------- /images&sql/products/product9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/images&sql/products/product9.jpg -------------------------------------------------------------------------------- /project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "client": "./client", 3 | "svr": "./server", 4 | "miniprogramRoot": "./client", 5 | "qcloudRoot": "./server", 6 | "setting": { 7 | "newFeature": true 8 | }, 9 | "appid": "touristappid", 10 | "projectname": "%E5%95%86%E5%9F%8E%E5%B0%8F%E7%A8%8B%E5%BA%8F", 11 | "condition": {} 12 | } -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # 腾讯云小程序解决方案 Demo - Node.js 2 | 3 | Node.js 版本 Wafer SDK 的服务端 Demo 4 | 5 | ## 下载源码 6 | 7 | 你可以直接通过 git 将代码 clone 到本地,也可以点击[这里](https://github.com/tencentyun/wafer-node-server-demo/releases)下载。 8 | 9 | ```bash 10 | git clone https://github.com/tencentyun/wafer-node-server-demo.git 11 | ``` 12 | 13 | ## 开始使用 14 | 15 | #### 安装依赖 16 | 17 | ```bash 18 | # 安装全局依赖 19 | npm i pm2 nodemon -g 20 | 21 | # 安装项目依赖 22 | npm i 23 | ``` 24 | 25 | #### 启动项目 26 | 27 | ```bash 28 | # 开发环境,监听文件变化自动重启,并会输出 debug 信息 29 | tnpm run dev 30 | 31 | # 线上部署环境 32 | tnpm start 33 | ``` 34 | 35 | 按照[小程序创建资源配置指引](https://github.com/tencentyun/weapp-doc)进行操作,可以得到运行本示例所需的资源和服务,其中包括已部署好的示例代码及自动下发的 SDK 配置文件 `/etc/qcloud/sdk.config`。 36 | 37 | - 示例代码部署目录:`/data/release/node-weapp-demo` 38 | - 运行示例的 Node 版本:`v8.1.0` 39 | - Node 进程管理工具:`pm2` 40 | 41 | ## 项目结构 42 | 43 | ``` 44 | koa-weapp-demo 45 | ├── README.md 46 | ├── app.js 47 | ├── controllers 48 | │ ├── index.js 49 | │ ├── login.js 50 | │ ├── message.js 51 | │ ├── tunnel.js 52 | │ ├── upload.js 53 | │ └── user.js 54 | ├── middlewares 55 | │ └── response.js 56 | ├── config.js 57 | ├── package.json 58 | ├── process.json 59 | ├── nodemon.json 60 | ├── qcloud.js 61 | └── routes 62 | └── index.js 63 | ``` 64 | `app.js` 是 Demo 的主入口文件,Demo 使用 Koa 框架,在 `app.js` 创建一个 Koa 实例并响应请求。 65 | 66 | `routes/index.js` 是 Demo 的路由定义文件 67 | 68 | `controllers` 存放 Demo 所有业务逻辑的目录,`index.js` 不需要修改,他会动态的将 `controllers` 文件夹下的目录结构映射成 modules 的 Object,例如 Demo 中的目录将会被映射成如下的结构: 69 | 70 | ```javascript 71 | // index.js 输出 72 | { 73 | login: require('login'), 74 | message: require('message'), 75 | tunnel: require('tunnel'), 76 | upload: require('upload'), 77 | user: require('user') 78 | } 79 | ``` 80 | 81 | `qcloud.js` 导出了一个 SDK 的单例,包含了所有的 SDK 接口,之后使用的时候只需要 `require` 这个文件就行,无需重复初始化 SDK。 82 | 83 | `config.js` 主要的配置如下: 84 | 85 | ```javascript 86 | { 87 | port: '5757', // 项目启动的端口 88 | 89 | appId: 'wx00dd00dd00dd00dd', // 微信小程序 App ID 90 | appSecret: 'abcdefg', // 微信小程序 App Secret 91 | wxLoginExpires: 7200, // 微信登录态有效期 92 | useQcloudLogin: false, // 是否使用腾讯云代理登录 93 | 94 | /** 95 | * MySQL 配置,用来存储用户登录态和用户信息 96 | * 如果不提供 MySQL 配置,模式会使用自动配置好的本地镜像中的 MySQL 储存信息 97 | * 具体查看文档-登录态储存和校验 98 | **/ 99 | mysql: { 100 | host: 'localhost', 101 | port: 3306, 102 | user: 'root', 103 | db: 'cAuth', 104 | pass: '', 105 | char: 'utf8' 106 | }, 107 | 108 | // COS 配置,用于上传模块使用 109 | cos: { 110 | /** 111 | * 区域 112 | * 华北:cn-north 113 | * 华东:cn-east 114 | * 华南:cn-south 115 | * 西南:cn-southwest 116 | */ 117 | region: 'cn-south', 118 | fileBucket: 'test', // Bucket 名称 119 | uploadFolder: '' // 文件夹 120 | } 121 | } 122 | ``` 123 | 124 | 除了 `config.js` ,腾讯云还会在你初始化小程序解决方案的时候,向你的机器下发 `sdk.config`,里面包含了你的腾讯云 AppId、SecretId、SecretKey 和服务器等信息,无需修改,`qcloud.js` 会自动引入。如果你想要在自己的机器上部署 SDK 的 Demo,请查看[自行部署 Demo 说明]()。 125 | 126 | 除此以外,关于 SDK 的详细配置信息,还可以查看 [SDK 的 API 文档]()。 -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa') 2 | const app = new Koa() 3 | const debug = require('debug')('koa-weapp-demo') 4 | const response = require('./middlewares/response') 5 | const bodyParser = require('koa-bodyparser') 6 | const config = require('./config') 7 | 8 | // 使用响应处理中间件 9 | app.use(response) 10 | 11 | // 解析请求体 12 | app.use(bodyParser()) 13 | 14 | // 引入路由分发 15 | const router = require('./routes') 16 | app.use(router.routes()) 17 | 18 | // 启动程序,监听端口 19 | app.listen(config.port, () => debug(`listening on port ${config.port}`)) 20 | -------------------------------------------------------------------------------- /server/config.js: -------------------------------------------------------------------------------- 1 | const CONF = { 2 | port: '5757', 3 | rootPathname: '', 4 | 5 | // 微信小程序 App ID 6 | appId: '', 7 | 8 | // 微信小程序 App Secret 9 | appSecret: '', 10 | 11 | // 是否使用腾讯云代理登录小程序 12 | useQcloudLogin: true, 13 | 14 | /** 15 | * MySQL 配置,用来存储 session 和用户信息 16 | * 若使用了腾讯云微信小程序解决方案 17 | * 开发环境下,MySQL 的初始密码为您的微信小程序 appid 18 | */ 19 | mysql: { 20 | host: 'localhost', 21 | port: 3306, 22 | user: 'root', 23 | db: 'cAuth', 24 | pass: '', 25 | char: 'utf8mb4' 26 | }, 27 | 28 | cos: { 29 | /** 30 | * 地区简称 31 | * @查看 https://cloud.tencent.com/document/product/436/6224 32 | */ 33 | region: 'ap-guangzhou', 34 | // Bucket 名称 35 | fileBucket: 'qcloudtest', 36 | // 文件夹 37 | uploadFolder: '' 38 | }, 39 | 40 | // 微信登录态有效期 41 | wxLoginExpires: 7200, 42 | wxMessageToken: 'abcdefgh' 43 | } 44 | 45 | module.exports = CONF 46 | -------------------------------------------------------------------------------- /server/controllers/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/wechat-term2-demo/4d58d8d3049b23d6c282ba7511f8aafbd2f49e84/server/controllers/.DS_Store -------------------------------------------------------------------------------- /server/controllers/comment.js: -------------------------------------------------------------------------------- 1 | const DB = require('../utils/db') 2 | 3 | module.exports = { 4 | 5 | /** 6 | * 添加评论 7 | */ 8 | add: async ctx => { 9 | let user = ctx.state.$wxInfo.userinfo.openId 10 | let username = ctx.state.$wxInfo.userinfo.nickName 11 | let avatar = ctx.state.$wxInfo.userinfo.avatarUrl 12 | 13 | let productId = +ctx.request.body.product_id 14 | let content = ctx.request.body.content || null 15 | 16 | let images = ctx.request.body.images || [] 17 | images = images.join(';;') 18 | 19 | 20 | if (!isNaN(productId)) { 21 | await DB.query('INSERT INTO comment(user, username, avatar, content, images, product_id) VALUES (?, ?, ?, ?, ?, ?)', [user, username, avatar, content, images, productId]) 22 | } 23 | 24 | ctx.state.data = {} 25 | }, 26 | 27 | /** 28 | * 获取评论列表 29 | */ 30 | list: async ctx => { 31 | let productId = +ctx.request.query.product_id 32 | 33 | if (!isNaN(productId)) { 34 | ctx.state.data = await DB.query('select * from comment where comment.product_id = ?', [productId]) 35 | } else { 36 | ctx.state.data = [] 37 | } 38 | }, 39 | } -------------------------------------------------------------------------------- /server/controllers/index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const fs = require('fs') 3 | const path = require('path') 4 | 5 | /** 6 | * 映射 d 文件夹下的文件为模块 7 | */ 8 | const mapDir = d => { 9 | const tree = {} 10 | 11 | // 获得当前文件夹下的所有的文件夹和文件 12 | const [dirs, files] = _(fs.readdirSync(d)).partition(p => fs.statSync(path.join(d, p)).isDirectory()) 13 | 14 | // 映射文件夹 15 | dirs.forEach(dir => { 16 | tree[dir] = mapDir(path.join(d, dir)) 17 | }) 18 | 19 | // 映射文件 20 | files.forEach(file => { 21 | if (path.extname(file) === '.js') { 22 | tree[path.basename(file, '.js')] = require(path.join(d, file)) 23 | } 24 | }) 25 | 26 | return tree 27 | } 28 | 29 | // 默认导出当前文件夹下的映射 30 | module.exports = mapDir(path.join(__dirname)) 31 | -------------------------------------------------------------------------------- /server/controllers/login.js: -------------------------------------------------------------------------------- 1 | // 登录授权接口 2 | module.exports = async (ctx, next) => { 3 | // 通过 Koa 中间件进行登录之后 4 | // 登录信息会被存储到 ctx.state.$wxInfo 5 | // 具体查看: 6 | if (ctx.state.$wxInfo.loginState) { 7 | ctx.state.data = ctx.state.$wxInfo.userinfo 8 | ctx.state.data['time'] = Math.floor(Date.now() / 1000) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /server/controllers/message.js: -------------------------------------------------------------------------------- 1 | const { message: { checkSignature } } = require('../qcloud') 2 | 3 | /** 4 | * 响应 GET 请求(响应微信配置时的签名检查请求) 5 | */ 6 | async function get (ctx, next) { 7 | const { signature, timestamp, nonce, echostr } = ctx.query 8 | if (checkSignature(signature, timestamp, nonce)) ctx.body = echostr 9 | else ctx.body = 'ERR_WHEN_CHECK_SIGNATURE' 10 | } 11 | 12 | async function post (ctx, next) { 13 | // 检查签名,确认是微信发出的请求 14 | const { signature, timestamp, nonce } = ctx.query 15 | if (!checkSignature(signature, timestamp, nonce)) ctx.body = 'ERR_WHEN_CHECK_SIGNATURE' 16 | 17 | /** 18 | * 解析微信发送过来的请求体 19 | * 可查看微信文档:https://mp.weixin.qq.com/debug/wxadoc/dev/api/custommsg/receive.html#接收消息和事件 20 | */ 21 | const body = ctx.request.body 22 | 23 | ctx.body = 'success' 24 | } 25 | 26 | module.exports = { 27 | post, 28 | get 29 | } 30 | -------------------------------------------------------------------------------- /server/controllers/order.js: -------------------------------------------------------------------------------- 1 | const DB = require('../utils/db.js') 2 | 3 | module.exports = { 4 | /** 5 | * 创建订单 6 | * 7 | */ 8 | add: async ctx => { 9 | let user = ctx.state.$wxInfo.userinfo.openId 10 | let productList = ctx.request.body.list || [] 11 | let isInstantBuy = !!ctx.request.body.isInstantBuy 12 | 13 | // 插入订单至 order_user 表 14 | let order = await DB.query('insert into order_user(user) values (?)', [user]) 15 | 16 | // 插入订单至 order_product 表 17 | let orderId = order.insertId 18 | let sql = 'INSERT INTO order_product (order_id, product_id, count) VALUES ' 19 | 20 | // 插入时所需要的数据和参数 21 | let query = [] 22 | let param = [] 23 | 24 | // 从购物车删除时所需要的数据和参数 25 | let needToDelQuery = [] 26 | let needToDelIds = [] 27 | 28 | productList.forEach(product => { 29 | query.push('(?, ?, ?)') 30 | 31 | param.push(orderId) 32 | param.push(product.id) 33 | param.push(product.count || 1) 34 | 35 | needToDelQuery.push('?') 36 | needToDelIds.push(product.id) 37 | 38 | }) 39 | 40 | await DB.query(sql + query.join(', '), param) 41 | 42 | if (!isInstantBuy) { 43 | // 非立即购买,购物车旧数据全部删除,此处本应使用事务实现,此处简化了 44 | await DB.query('DELETE FROM trolley_user WHERE trolley_user.id IN (' + needToDelQuery.join(', ') + ') AND trolley_user.user = ?', [...needToDelIds, user]) 45 | } 46 | 47 | ctx.state.data = {} 48 | 49 | }, 50 | 51 | /** 52 | * 获取已购买订单列表 53 | * 54 | */ 55 | list: async ctx => { 56 | let user = ctx.state.$wxInfo.userinfo.openId 57 | 58 | let list = await DB.query('SELECT order_user.id AS `id`, order_user.user AS `user`, order_user.create_time AS `create_time`, order_product.product_id AS `product_id`, order_product.count AS `count`, product.name AS `name`, product.image AS `image`, product.price AS `price` FROM order_user LEFT JOIN order_product ON order_user.id = order_product.order_id LEFT JOIN product ON order_product.product_id = product.id WHERE order_user.user = ? ORDER BY order_product.order_id', [user]) 59 | 60 | // 将数据库返回的数据组装成页面呈现所需的格式 61 | 62 | let ret = [] 63 | let cacheMap = {} 64 | let block = [] 65 | let id = 0 66 | list.forEach(order => { 67 | if (!cacheMap[order.id]) { 68 | block = [] 69 | ret.push({ 70 | id: ++id, 71 | list: block 72 | }) 73 | 74 | cacheMap[order.id] = true 75 | } 76 | 77 | block.push(order) 78 | }) 79 | 80 | ctx.state.data = ret 81 | }, 82 | 83 | } -------------------------------------------------------------------------------- /server/controllers/product.js: -------------------------------------------------------------------------------- 1 | const DB = require('../utils/db.js') 2 | 3 | module.exports = { 4 | /** 5 | * 拉取商品列表 6 | * 7 | */ 8 | 9 | list: async ctx => { 10 | ctx.state.data = await DB.query("SELECT * FROM product;") 11 | }, 12 | 13 | detail: async ctx => { 14 | let productId = + ctx.params.id 15 | let product 16 | 17 | if (!isNaN(productId)) { 18 | product = (await DB.query('select * from product where product.id = ?', [productId]))[0] 19 | } else { 20 | product = {} 21 | } 22 | 23 | product.commentCount = (await DB.query('SELECT COUNT(id) AS comment_count FROM comment WHERE comment.product_id = ?', [productId]))[0].comment_count || 0 24 | product.firstComment = (await DB.query('SELECT * FROM comment WHERE comment.product_id = ? LIMIT 1 OFFSET 0', [productId]))[0] || null 25 | 26 | ctx.state.data = product 27 | } 28 | } -------------------------------------------------------------------------------- /server/controllers/trolley.js: -------------------------------------------------------------------------------- 1 | const DB = require('../utils/db'); 2 | 3 | module.exports = { 4 | /** 5 | * 添加到购物车列表 6 | * 7 | */ 8 | add: async ctx => { 9 | let user = ctx.state.$wxInfo.userinfo.openId 10 | let product = ctx.request.body 11 | 12 | let list = await DB.query('SELECT * FROM trolley_user WHERE trolley_user.id = ? AND trolley_user.user = ?', [product.id, user]) 13 | 14 | if (!list.length) { 15 | // 商品还未添加到购物车 16 | await DB.query('INSERT INTO trolley_user(id, count, user) VALUES (?, ?, ?)', [product.id, 1, user]) 17 | } else { 18 | // 商品之前已经添加到购物车 19 | let count = list[0].count + 1 20 | await DB.query('UPDATE trolley_user SET count = ? WHERE trolley_user.id = ? AND trolley_user.user = ?', [count, product.id, user]) 21 | } 22 | 23 | ctx.state.data = {} 24 | 25 | 26 | }, 27 | 28 | /** 29 | * 拉取购物车商品列表 30 | * 31 | */ 32 | list: async ctx => { 33 | let user = ctx.state.$wxInfo.userinfo.openId 34 | 35 | ctx.state.data = await DB.query('SELECT * FROM trolley_user LEFT JOIN product ON trolley_user.id = product.id WHERE trolley_user.user = ?', [user]) 36 | }, 37 | 38 | /** 39 | * 更新购物车商品列表 40 | * 41 | */ 42 | update: async ctx => { 43 | let user = ctx.state.$wxInfo.userinfo.openId 44 | let productList = ctx.request.body.list || [] 45 | 46 | // 购物车旧数据全部删除 47 | await DB.query('DELETE FROM trolley_user WHERE trolley_user.user = ?', [user]) 48 | 49 | let sql = 'INSERT INTO trolley_user(id, count, user) VALUES ' 50 | let query = [] 51 | let param = [] 52 | 53 | productList.forEach(product => { 54 | query.push('(?, ?, ?)') 55 | 56 | param.push(product.id) 57 | param.push(product.count || 1) 58 | param.push(user) 59 | }) 60 | 61 | await DB.query(sql + query.join(', '), param) 62 | 63 | ctx.state.data = {} 64 | }, 65 | } -------------------------------------------------------------------------------- /server/controllers/tunnel.js: -------------------------------------------------------------------------------- 1 | const { tunnel } = require('../qcloud') 2 | const debug = require('debug')('koa-weapp-demo') 3 | 4 | /** 5 | * 这里实现一个简单的聊天室 6 | * userMap 为 tunnelId 和 用户信息的映射 7 | * 实际使用请使用数据库存储 8 | */ 9 | const userMap = {} 10 | 11 | // 保存 当前已连接的 WebSocket 信道ID列表 12 | const connectedTunnelIds = [] 13 | 14 | /** 15 | * 调用 tunnel.broadcast() 进行广播 16 | * @param {String} type 消息类型 17 | * @param {String} content 消息内容 18 | */ 19 | const $broadcast = (type, content) => { 20 | tunnel.broadcast(connectedTunnelIds, type, content) 21 | .then(result => { 22 | const invalidTunnelIds = result.data && result.data.invalidTunnelIds || [] 23 | 24 | if (invalidTunnelIds.length) { 25 | console.log('检测到无效的信道 IDs =>', invalidTunnelIds) 26 | 27 | // 从 userMap 和 connectedTunnelIds 中将无效的信道记录移除 28 | invalidTunnelIds.forEach(tunnelId => { 29 | delete userMap[tunnelId] 30 | 31 | const index = connectedTunnelIds.indexOf(tunnelId) 32 | if (~index) { 33 | connectedTunnelIds.splice(index, 1) 34 | } 35 | }) 36 | } 37 | }) 38 | } 39 | 40 | /** 41 | * 调用 TunnelService.closeTunnel() 关闭信道 42 | * @param {String} tunnelId 信道ID 43 | */ 44 | const $close = (tunnelId) => { 45 | tunnel.closeTunnel(tunnelId) 46 | } 47 | 48 | /** 49 | * 实现 onConnect 方法 50 | * 在客户端成功连接 WebSocket 信道服务之后会调用该方法, 51 | * 此时通知所有其它在线的用户当前总人数以及刚加入的用户是谁 52 | */ 53 | function onConnect (tunnelId) { 54 | console.log(`[onConnect] =>`, { tunnelId }) 55 | 56 | if (tunnelId in userMap) { 57 | connectedTunnelIds.push(tunnelId) 58 | 59 | $broadcast('people', { 60 | 'total': connectedTunnelIds.length, 61 | 'enter': userMap[tunnelId] 62 | }) 63 | } else { 64 | console.log(`Unknown tunnelId(${tunnelId}) was connectd, close it`) 65 | $close(tunnelId) 66 | } 67 | } 68 | 69 | /** 70 | * 实现 onMessage 方法 71 | * 客户端推送消息到 WebSocket 信道服务器上后,会调用该方法,此时可以处理信道的消息。 72 | * 在本示例,我们处理 `speak` 类型的消息,该消息表示有用户发言。 73 | * 我们把这个发言的信息广播到所有在线的 WebSocket 信道上 74 | */ 75 | function onMessage (tunnelId, type, content) { 76 | console.log(`[onMessage] =>`, { tunnelId, type, content }) 77 | 78 | switch (type) { 79 | case 'speak': 80 | if (tunnelId in userMap) { 81 | $broadcast('speak', { 82 | 'who': userMap[tunnelId], 83 | 'word': content.word 84 | }) 85 | } else { 86 | $close(tunnelId) 87 | } 88 | break 89 | 90 | default: 91 | break 92 | } 93 | } 94 | 95 | /** 96 | * 实现 onClose 方法 97 | * 客户端关闭 WebSocket 信道或者被信道服务器判断为已断开后, 98 | * 会调用该方法,此时可以进行清理及通知操作 99 | */ 100 | function onClose (tunnelId) { 101 | console.log(`[onClose] =>`, { tunnelId }) 102 | 103 | if (!(tunnelId in userMap)) { 104 | console.log(`[onClose][Invalid TunnelId]=>`, tunnelId) 105 | $close(tunnelId) 106 | return 107 | } 108 | 109 | const leaveUser = userMap[tunnelId] 110 | delete userMap[tunnelId] 111 | 112 | const index = connectedTunnelIds.indexOf(tunnelId) 113 | if (~index) { 114 | connectedTunnelIds.splice(index, 1) 115 | } 116 | 117 | // 聊天室没有人了(即无信道ID)不再需要广播消息 118 | if (connectedTunnelIds.length > 0) { 119 | $broadcast('people', { 120 | 'total': connectedTunnelIds.length, 121 | 'leave': leaveUser 122 | }) 123 | } 124 | } 125 | 126 | module.exports = { 127 | // 小程序请求 websocket 地址 128 | get: async ctx => { 129 | const data = await tunnel.getTunnelUrl(ctx.req) 130 | const tunnelInfo = data.tunnel 131 | 132 | userMap[tunnelInfo.tunnelId] = data.userinfo 133 | 134 | ctx.state.data = tunnelInfo 135 | }, 136 | 137 | // 信道将信息传输过来的时候 138 | post: async ctx => { 139 | const packet = await tunnel.onTunnelMessage(ctx.request.body) 140 | 141 | debug('Tunnel recive a package: %o', packet) 142 | 143 | switch (packet.type) { 144 | case 'connect': 145 | onConnect(packet.tunnelId) 146 | break 147 | case 'message': 148 | onMessage(packet.tunnelId, packet.content.messageType, packet.content.messageContent) 149 | break 150 | case 'close': 151 | onClose(packet.tunnelId) 152 | break 153 | } 154 | } 155 | 156 | } 157 | -------------------------------------------------------------------------------- /server/controllers/upload.js: -------------------------------------------------------------------------------- 1 | const { uploader } = require('../qcloud') 2 | 3 | module.exports = async ctx => { 4 | // 获取上传之后的结果 5 | // 具体可以查看: 6 | const data = await uploader(ctx.req) 7 | 8 | ctx.state.data = data 9 | } 10 | -------------------------------------------------------------------------------- /server/controllers/user.js: -------------------------------------------------------------------------------- 1 | module.exports = async (ctx, next) => { 2 | // 通过 Koa 中间件进行登录态校验之后 3 | // 登录信息会被存储到 ctx.state.$wxInfo 4 | // 具体查看: 5 | if (ctx.state.$wxInfo.loginState === 1) { 6 | // loginState 为 1,登录态校验成功 7 | ctx.state.data = ctx.state.$wxInfo.userinfo 8 | } else { 9 | ctx.state.code = -1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /server/middlewares/response.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('koa-weapp-demo') 2 | 3 | /** 4 | * 响应处理模块 5 | */ 6 | module.exports = async function (ctx, next) { 7 | try { 8 | // 调用下一个 middleware 9 | await next() 10 | 11 | // 处理响应结果 12 | // 如果直接写入在 body 中,则不作处理 13 | // 如果写在 ctx.body 为空,则使用 state 作为响应 14 | ctx.body = ctx.body ? ctx.body : { 15 | code: ctx.state.code !== undefined ? ctx.state.code : 0, 16 | data: ctx.state.data !== undefined ? ctx.state.data : {} 17 | } 18 | } catch (e) { 19 | // catch 住全局的错误信息 20 | debug('Catch Error: %o', e) 21 | 22 | // 设置状态码为 200 - 服务端错误 23 | ctx.status = 200 24 | 25 | // 输出详细的错误信息 26 | ctx.body = { 27 | code: -1, 28 | error: e && e.message ? e.message : e.toString() 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /server/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "restartable": "rs", 3 | "ignore": [ 4 | ".git", 5 | "node_modules/**/node_modules" 6 | ], 7 | "verbose": true, 8 | "execMap": { 9 | "js": "node --harmony" 10 | }, 11 | "env": { 12 | "NODE_ENV": "development", 13 | "DEBUG": "*,-nodemon:*,-nodemon,-knex:pool" 14 | }, 15 | "ext": "js json" 16 | } 17 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-weapp-demo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "pm2 start process.prod.json --no-daemon", 8 | "dev": "nodemon --config nodemon.json app.js", 9 | "initdb": "npm install && node tools/initdb.js" 10 | }, 11 | "author": "Jason", 12 | "license": "MIT", 13 | "dependencies": { 14 | "axios": "^0.15.3", 15 | "knex": "^0.13.0", 16 | "koa": "^2.0.0", 17 | "koa-bodyparser": "^3.2.0", 18 | "koa-log4": "^2.1.0", 19 | "koa-router": "^7.0.1", 20 | "lodash": "^4.17.4", 21 | "mkdir-p": "0.0.7", 22 | "mysql": "^2.14.1", 23 | "pify": "^2.3.0", 24 | "wafer-node-sdk": "^1.3.2" 25 | }, 26 | "devDependencies": { 27 | "babel-eslint": "^7.1.0", 28 | "debug": "^2.6.8", 29 | "eslint": "^3.9.1", 30 | "eslint-config-standard": "^6.2.1", 31 | "eslint-plugin-promise": "^3.3.1", 32 | "eslint-plugin-standard": "^2.0.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/process.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "session", 3 | "script": "app.js", 4 | "cwd": "./", 5 | "exec_mode": "fork", 6 | "watch": true, 7 | "ignore_watch": ["tmp"], 8 | "env": { 9 | "NODE_ENV": "production" 10 | }, 11 | "engines": { 12 | "node": ">=7.6" 13 | } 14 | } -------------------------------------------------------------------------------- /server/qcloud.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const qcloud = require('wafer-node-sdk') 3 | 4 | // 获取基础配置 5 | const configs = require('./config') 6 | 7 | // 获取 sdk.config 8 | const sdkConfig = (() => { 9 | const sdkConfigPath = '/data/release/sdk.config.json' 10 | 11 | // 检查文件是否存在 12 | try { 13 | const stats = fs.statSync(sdkConfigPath) 14 | 15 | if (!stats.isFile()) { 16 | console.log('sdk.config.json 不存在,将使用 config.js 中的配置') 17 | return {} 18 | } 19 | } catch (e) { 20 | return {} 21 | } 22 | 23 | // 返回配置信息 24 | try { 25 | const content = fs.readFileSync(sdkConfigPath, 'utf8') 26 | return JSON.parse(content) 27 | } catch (e) { 28 | // 如果配置读取错误或者 JSON 解析错误,则输出空配置项 29 | console.log('sdk.config.json 解析错误,不是 JSON 字符串') 30 | return {} 31 | } 32 | })() 33 | 34 | // 初始化 SDK 35 | // 将基础配置和 sdk.config 合并传入 SDK 并导出初始化完成的 SDK 36 | module.exports = qcloud(Object.assign({}, sdkConfig, configs)) 37 | -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ajax 服务路由集合 3 | */ 4 | const router = require('koa-router')({ 5 | prefix: '/weapp' 6 | }) 7 | const controllers = require('../controllers') 8 | 9 | // 从 sdk 中取出中间件 10 | // 这里展示如何使用 Koa 中间件完成登录态的颁发与验证 11 | const { auth: { authorizationMiddleware, validationMiddleware } } = require('../qcloud') 12 | 13 | // --- 登录与授权 Demo --- // 14 | // 登录接口 15 | router.get('/login', authorizationMiddleware, controllers.login) 16 | // 用户信息接口(可以用来验证登录态) 17 | router.get('/user', validationMiddleware, controllers.user) 18 | 19 | // --- 图片上传 Demo --- // 20 | // 图片上传接口,小程序端可以直接将 url 填入 wx.uploadFile 中 21 | router.post('/upload', controllers.upload) 22 | 23 | // --- 信道服务接口 Demo --- // 24 | // GET 用来响应请求信道地址的 25 | router.get('/tunnel', controllers.tunnel.get) 26 | // POST 用来处理信道传递过来的消息 27 | router.post('/tunnel', controllers.tunnel.post) 28 | 29 | // --- 客服消息接口 Demo --- // 30 | // GET 用来响应小程序后台配置时发送的验证请求 31 | router.get('/message', controllers.message.get) 32 | // POST 用来处理微信转发过来的客服消息 33 | router.post('/message', controllers.message.post) 34 | 35 | // 获取商品列表 36 | router.get('/product', controllers.product.list) 37 | 38 | // 获取商品详情 39 | router.get('/product/:id', controllers.product.detail) 40 | 41 | // 创建订单 42 | router.post('/order', validationMiddleware, controllers.order.add) 43 | 44 | // 显示已购买订单 45 | router.get('/order', validationMiddleware, controllers.order.list) 46 | 47 | // 商品添加到购物车列表 48 | router.put('/trolley', validationMiddleware, controllers.trolley.add) 49 | 50 | // 获取购物车商品列表 51 | router.get('/trolley', validationMiddleware, controllers.trolley.list) 52 | 53 | // 更新购物车商品列表 54 | router.post('/trolley', validationMiddleware, controllers.trolley.update) 55 | 56 | // 添加评论 57 | router.put('/comment', validationMiddleware, controllers.comment.add) 58 | 59 | // 获取评论列表 60 | router.get('/comment', controllers.comment.list) 61 | 62 | module.exports = router 63 | -------------------------------------------------------------------------------- /server/tools.md: -------------------------------------------------------------------------------- 1 | # 腾讯云小程序解决方案 Demo 工具使用文档 2 | 3 | 本文件夹下的脚本为腾讯云小程序解决方案 Demo 配套的工具,旨在让用户方便快捷的使用并创建小程序的开发环境。 4 | 5 | 工具包括: 6 | 7 | - [数据库初始化工具](#数据库初始化工具) 8 | 9 | ## 数据库初始化工具 10 | 11 | 本工具是为了让用户快速的按照腾讯云制定的数据库 schema 创建符合 SDK 标准的数据库结构。 12 | 13 | _**注意**:本工具支持的 MySQL 版本为 **5.7**,并且需提前在数据库中创建名为 `cAuth` 的数据库。`charset` 设置为 `utf8mb4`。_ 14 | 15 | 快速使用: 16 | 17 | ```bash 18 | npm run initdb 19 | ``` 20 | 21 | 或直接执行 `tools` 目录下的 `initdb.js` 文件: 22 | 23 | ```bash 24 | # 请保证已经执行了 npm install 安装了所需要的依赖 25 | node tools/initdb.js 26 | ``` 27 | 28 | 我们提供了初始化的 SQL 文件,你也可以用其他数据库工具(如 Navicat)直接导入 SQL 文件。 29 | -------------------------------------------------------------------------------- /server/tools/cAuth.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat Premium Data Transfer 3 | 4 | Source Server : Localhost 5 | Source Server Type : MySQL 6 | Source Server Version : 50717 7 | Source Host : localhost 8 | Source Database : cAuth 9 | 10 | Target Server Type : MySQL 11 | Target Server Version : 50717 12 | File Encoding : utf-8 13 | 14 | Date: 08/10/2017 22:22:52 PM 15 | */ 16 | 17 | SET NAMES utf8; 18 | SET FOREIGN_KEY_CHECKS = 0; 19 | 20 | -- ---------------------------- 21 | -- Table structure for `cSessionInfo` 22 | -- ---------------------------- 23 | DROP TABLE IF EXISTS `cSessionInfo`; 24 | CREATE TABLE `cSessionInfo` ( 25 | `open_id` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, 26 | `uuid` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, 27 | `skey` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, 28 | `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 29 | `last_visit_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 30 | `session_key` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, 31 | `user_info` varchar(2048) COLLATE utf8mb4_unicode_ci NOT NULL, 32 | PRIMARY KEY (`open_id`), 33 | KEY `openid` (`open_id`) USING BTREE, 34 | KEY `skey` (`skey`) USING BTREE 35 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='会话管理用户信息'; 36 | 37 | SET FOREIGN_KEY_CHECKS = 1; 38 | -------------------------------------------------------------------------------- /server/tools/initdb.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 腾讯云微信小程序解决方案 3 | * Demo 数据库初始化脚本 4 | * @author Jason 5 | */ 6 | const fs = require('fs') 7 | const path = require('path') 8 | const { mysql: config } = require('../config') 9 | 10 | console.log('\n======================================') 11 | console.log('开始初始化数据库...') 12 | 13 | // 初始化 SQL 文件路径 14 | const INIT_DB_FILE = path.join(__dirname, './cAuth.sql') 15 | 16 | const DB = require('knex')({ 17 | client: 'mysql', 18 | connection: { 19 | host: config.host, 20 | port: config.port, 21 | user: config.user, 22 | password: config.pass, 23 | database: config.db, 24 | charset: config.char, 25 | multipleStatements: true 26 | } 27 | }) 28 | 29 | console.log(`准备读取 SQL 文件:${INIT_DB_FILE}`) 30 | 31 | // 读取 .sql 文件内容 32 | const content = fs.readFileSync(INIT_DB_FILE, 'utf8') 33 | 34 | console.log('开始执行 SQL 文件...') 35 | 36 | // 执行 .sql 文件内容 37 | DB.raw(content).then(res => { 38 | console.log('数据库初始化成功!') 39 | process.exit(0) 40 | }, err => { 41 | throw new Error(err) 42 | }) 43 | -------------------------------------------------------------------------------- /server/utils/db.js: -------------------------------------------------------------------------------- 1 | const mysql = require('mysql') 2 | const config = require('../config').mysql 3 | 4 | var pool = null 5 | 6 | /** 7 | * 初始化连接池 8 | */ 9 | function initMysqlPool() { 10 | pool = mysql.createPool({ 11 | connectionLimit: 50, 12 | database: config.db, 13 | host: config.host, 14 | port: config.port, 15 | user: config.user, 16 | password: config.pass 17 | }); 18 | } 19 | 20 | module.exports = { 21 | /** 22 | * 执行sql查询 23 | */ 24 | query(sql, sqlParam, connection) { 25 | // 打印sql语句 26 | return new Promise((resolve, reject) => { 27 | if (connection) { 28 | connection.query(sql, sqlParam, (err, rows) => { 29 | if (err) { 30 | reject(err) 31 | } else { 32 | resolve(rows) 33 | } 34 | }) 35 | } else { 36 | if (!pool) { 37 | initMysqlPool() 38 | } 39 | 40 | pool.getConnection((err, connection) => { 41 | if (err) { 42 | reject(err) 43 | } else { 44 | connection.query(sql, sqlParam, (err, rows) => { 45 | connection.release() 46 | if (err) { 47 | reject(err) 48 | } else { 49 | resolve(rows) 50 | } 51 | }) 52 | } 53 | }) 54 | } 55 | }) 56 | }, 57 | } 58 | --------------------------------------------------------------------------------