├── .gitignore ├── README.md ├── cloudfunctions ├── benchmark │ ├── index.js │ └── package.json ├── cancelFollowing │ ├── index.js │ └── package.json ├── cancelLiked │ ├── index.js │ └── package.json ├── deletePoster │ ├── index.js │ └── package.json ├── getFollowers │ ├── index.js │ └── package.json ├── getFollowings │ ├── index.js │ └── package.json ├── getIfFollow │ ├── index.js │ └── package.json ├── getMainPageData │ ├── index.js │ └── package.json ├── getMePageData │ ├── index.js │ └── package.json ├── getPosterData │ ├── index.js │ └── package.json ├── getPosterLikes │ ├── index.js │ └── package.json ├── helper_test │ ├── config.json │ ├── index.js │ └── package.json ├── login │ ├── index.js │ └── package.json ├── openapi │ └── index.js └── searchUsers │ ├── index.js │ └── package.json ├── extra └── 小程序云开发实战-迷你微博.md ├── miniprogram ├── app.js ├── app.json ├── app.wxss ├── component │ └── image-wrapper │ │ ├── image-wrapper.js │ │ ├── image-wrapper.json │ │ ├── image-wrapper.wxml │ │ └── image-wrapper.wxss ├── images │ └── error.png ├── pages │ ├── circle │ │ ├── add-poster │ │ │ ├── add-poster.js │ │ │ ├── add-poster.json │ │ │ ├── add-poster.wxml │ │ │ └── add-poster.wxss │ │ ├── circle.js │ │ ├── circle.json │ │ ├── circle.wxml │ │ ├── circle.wxss │ │ ├── component │ │ │ ├── abstract-load │ │ │ │ ├── abstract-load.js │ │ │ │ ├── abstract-load.json │ │ │ │ ├── abstract-load.wxml │ │ │ │ └── abstract-load.wxss │ │ │ ├── post-item │ │ │ │ ├── post-item.js │ │ │ │ ├── post-item.json │ │ │ │ ├── post-item.wxml │ │ │ │ └── post-item.wxss │ │ │ └── user-info │ │ │ │ ├── user-info.js │ │ │ │ ├── user-info.json │ │ │ │ ├── user-info.wxml │ │ │ │ └── user-info.wxss │ │ ├── poster-detail │ │ │ ├── poster-detail.js │ │ │ ├── poster-detail.json │ │ │ ├── poster-detail.wxml │ │ │ └── poster-detail.wxss │ │ ├── search-user │ │ │ ├── search-user.js │ │ │ ├── search-user.json │ │ │ ├── search-user.wxml │ │ │ └── search-user.wxss │ │ ├── user-data │ │ │ ├── user-data.js │ │ │ ├── user-data.json │ │ │ ├── user-data.wxml │ │ │ └── user-data.wxss │ │ ├── user-follower │ │ │ ├── user-follower.js │ │ │ ├── user-follower.json │ │ │ ├── user-follower.wxml │ │ │ └── user-follower.wxss │ │ ├── user-following │ │ │ ├── user-following.js │ │ │ ├── user-following.json │ │ │ ├── user-following.wxml │ │ │ └── user-following.wxss │ │ └── user-poster │ │ │ ├── user-poster.js │ │ │ ├── user-poster.json │ │ │ ├── user-poster.wxml │ │ │ └── user-poster.wxss │ └── index │ │ ├── index.js │ │ ├── index.json │ │ ├── index.wxml │ │ ├── index.wxss │ │ └── user-unlogin.png ├── sitemap.json └── style │ └── guide.wxss └── project.config.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows 2 | [Dd]esktop.ini 3 | Thumbs.db 4 | $RECYCLE.BIN/ 5 | 6 | # macOS 7 | .DS_Store 8 | .fseventsd 9 | .Spotlight-V100 10 | .TemporaryItems 11 | .Trashes 12 | 13 | # Node.js 14 | node_modules/ 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### KuthorX-Helper 2 | 3 | 教你手把手写小程序X云开发! 4 | 5 | [小程序云开发实战-迷你微博](./extra/小程序云开发实战-迷你微博.md) 6 | -------------------------------------------------------------------------------- /cloudfunctions/benchmark/index.js: -------------------------------------------------------------------------------- 1 | // 云函数入口文件 2 | const cloud = require('wx-server-sdk') 3 | cloud.init() 4 | const db = cloud.database() 5 | 6 | // 云函数入口函数 7 | exports.main = async (event, context) => { 8 | const test = await db.collection("poster").where({ 9 | authorId: "93db88df603dbeccac52f65c68dfc21c86ff2cbb" 10 | }).get() 11 | return test 12 | } -------------------------------------------------------------------------------- /cloudfunctions/benchmark/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "benchmark", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wx-server-sdk": "latest" 13 | } 14 | } -------------------------------------------------------------------------------- /cloudfunctions/cancelFollowing/index.js: -------------------------------------------------------------------------------- 1 | // 云函数入口文件 2 | const cloud = require('wx-server-sdk') 3 | cloud.init() 4 | const db = cloud.database() 5 | 6 | // 云函数入口函数 7 | exports.main = async(event, context) => { 8 | const result = await db.collection("poster_user_follows") 9 | .where({ 10 | followerId: event.followerId, 11 | followingId: event.followingId, 12 | }) 13 | .remove() 14 | return result 15 | } -------------------------------------------------------------------------------- /cloudfunctions/cancelFollowing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cancelFollowing", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wx-server-sdk": "latest" 13 | } 14 | } -------------------------------------------------------------------------------- /cloudfunctions/cancelLiked/index.js: -------------------------------------------------------------------------------- 1 | // 云函数入口文件 2 | const cloud = require('wx-server-sdk') 3 | cloud.init() 4 | const db = cloud.database() 5 | 6 | // 云函数入口函数 7 | exports.main = async (event, context) => { 8 | const result = await db.collection("poster_likes") 9 | .where({ 10 | posterId: event.posterId, 11 | likeId: event.likeId, 12 | }) 13 | .remove() 14 | return result 15 | } -------------------------------------------------------------------------------- /cloudfunctions/cancelLiked/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cancelLiked", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wx-server-sdk": "latest" 13 | } 14 | } -------------------------------------------------------------------------------- /cloudfunctions/deletePoster/index.js: -------------------------------------------------------------------------------- 1 | // 云函数入口文件 2 | const cloud = require('wx-server-sdk') 3 | cloud.init() 4 | const db = cloud.database() 5 | 6 | // 云函数入口函数 7 | exports.main = async(event, context) => { 8 | const result = await db.collection("poster").doc(event._id) 9 | .remove() 10 | return result 11 | } -------------------------------------------------------------------------------- /cloudfunctions/deletePoster/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deletePoster", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wx-server-sdk": "latest" 13 | } 14 | } -------------------------------------------------------------------------------- /cloudfunctions/getFollowers/index.js: -------------------------------------------------------------------------------- 1 | // 云函数入口文件 2 | const cloud = require('wx-server-sdk') 3 | cloud.init() 4 | const db = cloud.database() 5 | 6 | // 云函数入口函数 7 | exports.main = async (event, context) => { 8 | const userId = event.userId 9 | const result = await db.collection("poster_user_follows").where({ 10 | followingId: userId 11 | }).get() 12 | const followerIds = result.data.map(v => { 13 | return v.followerId 14 | }) 15 | const followerInfos = await db.collection("poster_users").where({ 16 | userId: db.command.in(followerIds) 17 | }).get() 18 | return followerInfos 19 | } -------------------------------------------------------------------------------- /cloudfunctions/getFollowers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getFollowers", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wx-server-sdk": "latest" 13 | } 14 | } -------------------------------------------------------------------------------- /cloudfunctions/getFollowings/index.js: -------------------------------------------------------------------------------- 1 | // 云函数入口文件 2 | const cloud = require('wx-server-sdk') 3 | cloud.init() 4 | const db = cloud.database() 5 | 6 | // 云函数入口函数 7 | exports.main = async (event, context) => { 8 | const userId = event.userId 9 | const result = await db.collection("poster_user_follows").where({ 10 | followerId: userId 11 | }).get() 12 | const followingIds = result.data.map(v => { 13 | return v.followingId 14 | }) 15 | const followingInfos = await db.collection("poster_users").where({ 16 | userId: db.command.in(followingIds) 17 | }).get() 18 | return followingInfos 19 | } -------------------------------------------------------------------------------- /cloudfunctions/getFollowings/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getFollowings", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wx-server-sdk": "latest" 13 | } 14 | } -------------------------------------------------------------------------------- /cloudfunctions/getIfFollow/index.js: -------------------------------------------------------------------------------- 1 | // 云函数入口文件 2 | const cloud = require('wx-server-sdk') 3 | cloud.init() 4 | const db = cloud.database() 5 | 6 | // 云函数入口函数 7 | exports.main = async(event, context) => { 8 | const followingResult = await db.collection("poster_user_follows") 9 | .where({ 10 | followingId: event.followingId, 11 | followerId: event.followerId 12 | }).get() 13 | return followingResult 14 | } -------------------------------------------------------------------------------- /cloudfunctions/getIfFollow/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getIfFollow", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wx-server-sdk": "latest" 13 | } 14 | } -------------------------------------------------------------------------------- /cloudfunctions/getMainPageData/index.js: -------------------------------------------------------------------------------- 1 | // 云函数入口文件 2 | const cloud = require("wx-server-sdk") 3 | cloud.init() 4 | const db = cloud.database() 5 | 6 | const options = { 7 | year: "numeric", 8 | month: "numeric", 9 | day: "numeric", 10 | hour: "numeric", 11 | minute: "numeric", 12 | second: "numeric", 13 | hourCycle: "h23" 14 | } 15 | 16 | const MAX_LIMIT = 100 17 | 18 | async function getDbData(dbName, whereObj) { 19 | const totalCountsData = await db 20 | .collection(dbName) 21 | .where(whereObj) 22 | .count() 23 | const total = totalCountsData.total 24 | console.log(total) 25 | const batch = Math.ceil(total / 100) 26 | const tasks = [] 27 | for (let i = 0; i < batch; i++) { 28 | const promise = db 29 | .collection(dbName) 30 | .where(whereObj) 31 | .skip(i * MAX_LIMIT) 32 | .limit(MAX_LIMIT) 33 | .get() 34 | tasks.push(promise) 35 | } 36 | const rrr = await Promise.all(tasks) 37 | if (rrr.length !== 0) { 38 | return rrr.reduce((acc, cur) => { 39 | console.log(acc) 40 | return { 41 | data: acc.data.concat(cur.data), 42 | errMsg: acc.errMsg 43 | } 44 | }) 45 | } else { 46 | return { 47 | data: [], 48 | errMsg: "empty" 49 | } 50 | } 51 | } 52 | 53 | // 云函数入口函数 54 | exports.main = async (event, context, cb) => { 55 | const userId = event.userId 56 | const isEveryOne = event.isEveryOne 57 | const isOnlyMe = event.isOnlyMe 58 | let followingResult 59 | let users 60 | let idNameMap = {} 61 | let followingIds = [] 62 | if (isEveryOne) { 63 | // followingResult = await getDbData("poster_users", {}) 64 | followingResult = await db.collection("poster_users").get() 65 | users = followingResult.data 66 | followingIds = users.map(u => { 67 | return u.userId 68 | }) 69 | } else if (isOnlyMe) { 70 | // followingResult = await getDbData("poster_users", { 71 | // userId: userId 72 | // }) 73 | followingResult = await db 74 | .collection("poster_users") 75 | .where({ 76 | userId: userId 77 | }) 78 | .get() 79 | users = followingResult.data 80 | followingIds = users.map(u => { 81 | return u.userId 82 | }) 83 | } else { 84 | // followingResult = await getDbData("poster_user_follows", { 85 | // followerId: userId 86 | // }) 87 | followingResult = await db 88 | .collection("poster_user_follows") 89 | .where({ 90 | followerId: userId 91 | }) 92 | .get() 93 | const following = followingResult.data 94 | followingIds = following.map(f => { 95 | return f.followingId 96 | }) 97 | followingIds.push(userId) 98 | // const userData = await getDbData("poster_users", { 99 | // userId: db.command.in(followingIds) 100 | // }) 101 | userData = await db 102 | .collection("poster_users") 103 | .where({ 104 | userId: db.command.in(followingIds) 105 | }) 106 | .get() 107 | users = userData.data 108 | } 109 | users.map(u => { 110 | idNameMap[u.userId] = u.name 111 | }) 112 | console.log(idNameMap) 113 | // 获取动态 114 | // const postResult = await getDbData("poster", { 115 | // authorId: db.command.in(followingIds) 116 | // }) 117 | const postResult = await db 118 | .collection("poster") 119 | .orderBy("date", "desc") 120 | .where({ 121 | authorId: db.command.in(followingIds) 122 | }) 123 | .get() 124 | const postData = postResult.data 125 | console.log(postData) 126 | postData.map(p => { 127 | p.author = idNameMap[p.authorId] 128 | p.formatDate = new Date(p.date).toLocaleDateString("zh-Hans", options) 129 | }) 130 | console.log(postData) 131 | return postData 132 | } 133 | -------------------------------------------------------------------------------- /cloudfunctions/getMainPageData/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getMainPageData", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wx-server-sdk": "latest" 13 | } 14 | } -------------------------------------------------------------------------------- /cloudfunctions/getMePageData/index.js: -------------------------------------------------------------------------------- 1 | // 云函数入口文件 2 | const cloud = require("wx-server-sdk") 3 | cloud.init() 4 | const db = cloud.database() 5 | 6 | async function getPosterCount(userId) { 7 | return { 8 | value: (await db.collection("poster").where({ 9 | authorId: userId 10 | }).count()).total, 11 | key: "posterCount" 12 | } 13 | } 14 | 15 | async function getFollowingCount(userId) { 16 | return { 17 | value: (await db.collection("poster_user_follows").where({ 18 | followerId: userId 19 | }).count()).total, 20 | key: "followingCount" 21 | } 22 | } 23 | 24 | async function getFollowerCount(userId) { 25 | return { 26 | value: (await db.collection("poster_user_follows").where({ 27 | followingId: userId 28 | }).count()).total, 29 | key: "followerCount" 30 | } 31 | } 32 | 33 | 34 | async function getUserName(userId) { 35 | return { 36 | value: (await db.collection("poster_users").where({ 37 | userId: userId 38 | }).get()).data[0].name, 39 | key: "userName" 40 | } 41 | } 42 | 43 | // 云函数入口函数 44 | exports.main = async (event, context) => { 45 | const userId = event.userId 46 | // const userName = await db.collection("poster_users").where({ 47 | // userId: userId 48 | // }) 49 | // const posterCount = await db.collection("poster").where({ 50 | // authorId: userId 51 | // }).count() 52 | // const followingCount = await db.collection("poster_user_follows").where({ 53 | // followerId: userId 54 | // }).count() 55 | // const followerCount = await db.collection("poster_user_follows").where({ 56 | // followingId: userId 57 | // }).count() 58 | 59 | const tasks = [] 60 | tasks.push(getPosterCount(userId)) 61 | tasks.push(getFollowerCount(userId)) 62 | tasks.push(getFollowingCount(userId)) 63 | tasks.push(getUserName(userId)) 64 | 65 | const allData = await Promise.all(tasks) 66 | const finalData = {} 67 | allData.map(d => { 68 | finalData[d.key] = d.value 69 | }) 70 | 71 | return finalData 72 | } -------------------------------------------------------------------------------- /cloudfunctions/getMePageData/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getMePageData", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wx-server-sdk": "latest" 13 | } 14 | } -------------------------------------------------------------------------------- /cloudfunctions/getPosterData/index.js: -------------------------------------------------------------------------------- 1 | // 云函数入口文件 2 | const cloud = require("wx-server-sdk") 3 | cloud.init() 4 | const db = cloud.database() 5 | 6 | const options = { 7 | year: "numeric", 8 | month: "numeric", 9 | day: "numeric", 10 | hour: "numeric", 11 | minute: "numeric", 12 | second: "numeric", 13 | hourCycle: "h23" 14 | } 15 | 16 | // 云函数入口函数 17 | exports.main = async (event, context) => { 18 | const posterData = await db.collection("poster").doc(event.posterId).get() 19 | const posterInner = posterData.data 20 | const authorId = posterInner.authorId 21 | const authorInfo = await db.collection("poster_users").where({ 22 | userId: authorId 23 | }).get() 24 | const name = authorInfo.data[0].name 25 | posterInner.author = name 26 | posterInner.formatDate = new Date(posterInner.date).toLocaleDateString("zh-Hans", options) 27 | return posterInner 28 | } -------------------------------------------------------------------------------- /cloudfunctions/getPosterData/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getPosterData", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wx-server-sdk": "latest" 13 | } 14 | } -------------------------------------------------------------------------------- /cloudfunctions/getPosterLikes/index.js: -------------------------------------------------------------------------------- 1 | // 云函数入口文件 2 | const cloud = require("wx-server-sdk") 3 | cloud.init() 4 | const db = cloud.database() 5 | 6 | const options = { 7 | year: "numeric", 8 | month: "numeric", 9 | day: "numeric", 10 | hour: "numeric", 11 | minute: "numeric", 12 | second: "numeric", 13 | hourCycle: "h23" 14 | } 15 | 16 | // 云函数入口函数 17 | exports.main = async (event, context) => { 18 | const likesData = await db.collection("poster_likes").where({ 19 | posterId: event.posterId 20 | }).get() 21 | const likesInner = likesData.data 22 | const likeUserIds = likesInner.map(l => { 23 | return l.likeId 24 | }) 25 | const userInfos = await db.collection("poster_users").where({ 26 | userId: db.command.in(likeUserIds) 27 | }).get() 28 | const userInner = userInfos.data 29 | const idNameMap = {} 30 | userInner.map(u => { 31 | idNameMap[u.userId] = u.name 32 | }) 33 | likesInner.map(l => { 34 | l.name = idNameMap[l.likeId] 35 | }) 36 | return likesInner 37 | } -------------------------------------------------------------------------------- /cloudfunctions/getPosterLikes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getPosterLikes", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wx-server-sdk": "latest" 13 | } 14 | } -------------------------------------------------------------------------------- /cloudfunctions/helper_test/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "openapi": [ 4 | "customerServiceMessage.send" 5 | ] 6 | } 7 | } -------------------------------------------------------------------------------- /cloudfunctions/helper_test/index.js: -------------------------------------------------------------------------------- 1 | // 云函数入口文件 2 | const cloud = require('wx-server-sdk') 3 | 4 | cloud.init() 5 | 6 | function getRandomInt(max) { 7 | return Math.floor(Math.random() * Math.floor(max)); 8 | } 9 | 10 | 11 | // 云函数入口函数 12 | exports.main = async(event, context) => { 13 | const wxContext = cloud.getWXContext() 14 | console.log(event) 15 | 16 | const content = event.Content 17 | if (content === "推理小说") { 18 | const db = cloud.database() 19 | const countResult = await db.collection('books').count() 20 | const id = getRandomInt(countResult.total) + 1 21 | const result = await db.collection('books').where({ 22 | id: id 23 | }) 24 | .get() 25 | const data = result.data[0] 26 | const resultString = `中文基本信息:\n${data.ch}\n英文基本信息:\n${data.en}\n基本介绍:\n${data.intro}` 27 | await cloud.openapi.customerServiceMessage.send({ 28 | touser: wxContext.OPENID, 29 | msgtype: 'text', 30 | text: { 31 | content: resultString, 32 | }, 33 | }) 34 | 35 | return 'success' 36 | } 37 | 38 | const test = { 39 | "ToUserName": "toUser", 40 | "FromUserName": "fromUser", 41 | "CreateTime": 1482048670, 42 | "MsgType": "text", 43 | "Content": "this is a test", 44 | "MsgId": 1234567890123456 45 | } 46 | 47 | await cloud.openapi.customerServiceMessage.send({ 48 | touser: wxContext.OPENID, 49 | msgtype: 'text', 50 | text: { 51 | content: '不识别的信息哦,请输入其他信息吧!\n目前客服支持:\n1. 推理小说推荐:发送“推理小说”即可随机获得一本推理小说信息\n即将上线:\n1. 淘金记', 52 | }, 53 | }) 54 | 55 | // return 'success' 56 | } -------------------------------------------------------------------------------- /cloudfunctions/helper_test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "helper_test", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wx-server-sdk": "latest" 13 | } 14 | } -------------------------------------------------------------------------------- /cloudfunctions/login/index.js: -------------------------------------------------------------------------------- 1 | // 云函数模板 2 | // 部署:在 cloud-functions/login 文件夹右击选择 “上传并部署” 3 | 4 | const cloud = require('wx-server-sdk') 5 | 6 | // 初始化 cloud 7 | cloud.init() 8 | 9 | /** 10 | * 这个示例将经自动鉴权过的小程序用户 openid 返回给小程序端 11 | * 12 | * event 参数包含小程序端调用传入的 data 13 | * 14 | */ 15 | exports.main = (event, context) => { 16 | console.log(event) 17 | console.log(context) 18 | 19 | // 可执行其他自定义逻辑 20 | // console.log 的内容可以在云开发云函数调用日志查看 21 | 22 | // 获取 WX Context (微信调用上下文),包括 OPENID、APPID、及 UNIONID(需满足 UNIONID 获取条件) 23 | const wxContext = cloud.getWXContext() 24 | 25 | return { 26 | event, 27 | openid: wxContext.OPENID, 28 | appid: wxContext.APPID, 29 | unionid: wxContext.UNIONID, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /cloudfunctions/login/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "login", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wx-server-sdk": "latest" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /cloudfunctions/openapi/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KuthorX/KuthorX-Helper/03ac0aba45ac1b44b6f485a0eb2d8e340c1ae49d/cloudfunctions/openapi/index.js -------------------------------------------------------------------------------- /cloudfunctions/searchUsers/index.js: -------------------------------------------------------------------------------- 1 | // 云函数入口文件 2 | const cloud = require('wx-server-sdk') 3 | cloud.init() 4 | const db = cloud.database() 5 | 6 | const MAX_LIMIT = 100 7 | 8 | async function getDbData(dbName, whereObj) { 9 | const totalCountsData = await db.collection(dbName).where(whereObj).count() 10 | const total = totalCountsData.total 11 | const batch = Math.ceil(total / 100) 12 | const tasks = [] 13 | for (let i = 0; i < batch; i++) { 14 | const promise = db 15 | .collection(dbName) 16 | .where(whereObj) 17 | .skip(i * MAX_LIMIT) 18 | .limit(MAX_LIMIT) 19 | .get() 20 | tasks.push(promise) 21 | } 22 | const rrr = await Promise.all(tasks) 23 | if (rrr.length !== 0) { 24 | return rrr.reduce((acc, cur) => { 25 | return { 26 | data: acc.data.concat(cur.data), 27 | errMsg: acc.errMsg 28 | } 29 | }) 30 | } else { 31 | return { 32 | data: [], 33 | errMsg: "empty" 34 | } 35 | } 36 | } 37 | 38 | // 云函数入口函数 39 | exports.main = async (event, context) => { 40 | const text = event.text 41 | const data = await getDbData("poster_users", { 42 | name: { 43 | $regex: text 44 | } 45 | }) 46 | return data 47 | } -------------------------------------------------------------------------------- /cloudfunctions/searchUsers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "searchUsers", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wx-server-sdk": "latest" 13 | } 14 | } -------------------------------------------------------------------------------- /extra/小程序云开发实战-迷你微博.md: -------------------------------------------------------------------------------- 1 | ### 0. 前言 2 | 3 | 本文将手把手教你如何写出迷你版微博的一行行代码,迷你版微博包含以下功能: 4 | 5 | - Feed 流:关注动态、所有动态 6 | - 发送图文动态 7 | - 搜索用户 8 | - 关注系统 9 | - 点赞动态 10 | - 个人主页 11 | 12 | 使用到的云开发能力: 13 | 14 | - 云数据库 15 | - 云存储 16 | - 云函数 17 | - 云调用 18 | 19 | 没错,几乎是所有的云开发能力。也就是说,读完这篇实战,你就相当于完全入门了云开发! 20 | 21 | 咳咳,当然,实际上这里只是介绍核心逻辑和重点代码片段,完整代码建议下载查看。 22 | 23 | ### 1. 取得授权 24 | 25 | 作为一个社交平台,首先要做的肯定是经过用户授权,获取用户信息,小程序提供了很方便的接口: 26 | 27 | ```html 28 | 31 | ``` 32 | 33 | 这个 `button` 有个 `open-type` 属性,这个属性是专门用来使用小程序的开放能力的,而 `getUserInfo` 则表示 **获取用户信息,可以从`bindgetuserinfo`回调中获取到用户信息**。 34 | 35 | 于是我们可以在 wxml 里放入这个 `button` 后,在相应的 js 里写如下代码: 36 | 37 | ```js 38 | Page({ 39 | ... 40 | 41 | getUserInfo: function(e) { 42 | wx.navigateTo({ 43 | url: "/pages/circle/circle" 44 | }) 45 | }, 46 | 47 | ... 48 | }) 49 | ``` 50 | 51 | 这样在成功获取到用户信息后,我们就能跳转到迷你微博页面了。 52 | 53 | 需要注意,不能使用 `wx.authorize({scope: "scope.userInfo"})` 来获取读取用户信息的权限,因为它不会跳出授权弹窗。目前只能使用上面所述的方式实现。 54 | 55 | ### 2. 主页设计 56 | 57 | 社交平台的主页大同小异,主要由三个部分组成: 58 | 59 | - Feed 流 60 | - 消息 61 | - 个人信息 62 | 63 | 那么很容易就能想到这样的布局(注意新建一个 Page 哦,路径:`pages/circle/circle.wxml`): 64 | 65 | ```html 66 | 67 | 71 | 72 | 73 | 77 | 78 | 79 | 83 | 84 | 85 | 86 | 87 | 94 | 95 | 96 | 103 | 104 | 105 | 112 | 113 | 114 | 115 | ``` 116 | 117 | 很好理解,画面主要被分为上下两个部分:上面的部分是主要内容,下面的部分是三个 Tab 组成的 Footer。重点 WXSS 实现(完整的 WXSS 可以下载源码查看): 118 | 119 | ```css 120 | .footer { 121 | box-shadow: 0 0 15rpx #ccc; 122 | display: flex; 123 | position: fixed; 124 | height: 120rpx; 125 | bottom: 0; 126 | width: 100%; 127 | flex-direction: row; 128 | justify-content: center; 129 | z-index: 100; 130 | background: #fff; 131 | } 132 | 133 | .footer-item { 134 | display: flex; 135 | justify-content: center; 136 | align-items: center; 137 | height: 100%; 138 | width: 33.33%; 139 | color: #333; 140 | } 141 | 142 | .footer-item:nth-child(2) { 143 | border-left: 3rpx solid #aaa; 144 | border-right: 3rpx solid #aaa; 145 | flex-grow: 1; 146 | } 147 | 148 | .footer-btn { 149 | width: 100%; 150 | height: 100%; 151 | display: flex; 152 | justify-content: center; 153 | align-items: center; 154 | border-radius: 0; 155 | font-size: 30rpx; 156 | } 157 | ``` 158 | 159 | 核心逻辑是通过 `position: fixed` 来让 Footer 一直在下方。 160 | 161 | 读者会发现有一个 `currentPage` 的 data ,这个 data 的作用其实很直观:通过判断它的值是 `main`/`msg`/`me` 中的哪一个来决定主要内容。同时,为了让首次使用的用户知道自己在哪个 Tab,Footer 中相应的 `button` 也会从白底黑字黑底白字,与另外两个 Tab 形成对比。 162 | 163 | 现在我们来看看 `main` 部分的代码(在上面代码的基础上扩充): 164 | 165 | ```html 166 | ... 167 | 171 | 172 | 178 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 191 | 192 | 198 | 199 | 200 | 无数据 203 | 204 | 212 | 213 | ... 214 | ``` 215 | 216 | 这里用到了 [列表渲染](https://developers.weixin.qq.com/miniprogram/dev/reference/wxml/list.html) 和 [条件渲染](https://developers.weixin.qq.com/miniprogram/dev/reference/wxml/conditional.html),还不清楚的可以点击进去学习一下。 217 | 218 | 可以看到,相比之前的代码,我添加一个 header,同时 `main-area` 的内部也新增了一个 `scroll-view`(用于展示 Feed 流) 和一个 `button`(用于编辑新迷你微博)。header 的功能很简单:左侧区域是一个 `picker`,可以选择查看的动态类型(目前有 _关注动态_ 和 _所有动态_ 两种);右侧区域是一个按钮,点击后可以跳转到搜索页面,这两个功能我们先放一下,先继续看 `main-area` 的新增内容。 219 | 220 | `main-area` 里的 `scroll-view` 是一个可监听滚动事件的列表,其中监听事件的实现: 221 | 222 | ```js 223 | data: { 224 | ... 225 | addPosterBtnBottom: "190rpx", 226 | mainHeaderMaxHeight: "80rpx", 227 | mainAreaHeight: "calc(100vh - 200rpx)", 228 | mainAreaMarginTop: "80rpx", 229 | }, 230 | onMainPageScroll: function(e) { 231 | if (e.detail.deltaY < 0) { 232 | this.setData({ 233 | addPosterBtnBottom: "-190rpx", 234 | mainHeaderMaxHeight: "0", 235 | mainAreaHeight: "calc(100vh - 120rpx)", 236 | mainAreaMarginTop: "0rpx" 237 | }) 238 | } else { 239 | this.setData({ 240 | addPosterBtnBottom: "190rpx", 241 | mainHeaderMaxHeight: "80rpx", 242 | mainAreaHeight: "calc(100vh - 200rpx)", 243 | mainAreaMarginTop: "80rpx" 244 | }) 245 | } 246 | }, 247 | ... 248 | ``` 249 | 250 | 结合 wxml 可以知道,当页面向下滑动 (deltaY < 0) 时,header 和 `button` 会 “突然消失”,反之它们则会 “突然出现”。为了视觉上有更好地过渡,我们可以在 WXSS 中使用 `transition` : 251 | 252 | ```css 253 | ... 254 | .main-area { 255 | position: relative; 256 | flex-grow: 1; 257 | overflow: auto; 258 | z-index: 1; 259 | transition: height 0.3s, margin-top 0.3s; 260 | } 261 | .main-header { 262 | position: fixed; 263 | width: 100%; 264 | height: 80rpx; 265 | background: #fff; 266 | top: 0; 267 | left: 0; 268 | display: flex; 269 | justify-content: space-around; 270 | align-items: center; 271 | z-index: 100; 272 | border-bottom: 3rpx solid #aaa; 273 | transition: max-height 0.3s; 274 | overflow: hidden; 275 | } 276 | .add-poster-btn { 277 | position: fixed; 278 | right: 60rpx; 279 | box-shadow: 5rpx 5rpx 10rpx #aaa; 280 | display: flex; 281 | justify-content: center; 282 | align-items: center; 283 | color: #333; 284 | padding-bottom: 10rpx; 285 | text-align: center; 286 | border-radius: 50%; 287 | font-size: 60rpx; 288 | width: 100rpx; 289 | height: 100rpx; 290 | transition: bottom 0.3s; 291 | background: #fff; 292 | z-index: 1; 293 | } 294 | ... 295 | ``` 296 | 297 | ### 3. Feed 流 298 | 299 | #### 3.1 post-item 300 | 301 | 前面提到,`scroll-view` 的内容是 Feed 流,那么首先就要想到使用 [列表渲染](https://developers.weixin.qq.com/miniprogram/dev/reference/wxml/list.html)。而且,为了方便在个人主页复用,列表渲染中的每一个 item 都要抽象出来。这时就要使用小程序中的 [Custom-Component](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/) 功能了。 302 | 303 | 新建一个名为 `post-item` 的 `Component`,其中 wxml 的实现(路径:`pages/circle/component/post-item/post-item.js`): 304 | 305 | ```html 306 | 312 | 313 | {{data.author}} 316 | {{data.formatDate}} 317 | 318 | 319 | {{data.msg}} 320 | 321 | 322 | 323 | 324 | 325 | ``` 326 | 327 | 可见,一个 `poster-item` 最主要有以下信息: 328 | 329 | - 作者名 330 | - 发送时间 331 | - 文本内容 332 | - 图片内容 333 | 334 | 其中,图片内容因为是可选的,所以使用了 [条件渲染](https://developers.weixin.qq.com/miniprogram/dev/reference/wxml/conditional.html),这会在没有图片信息时不让图片显示区域占用屏幕空间。另外,图片内容主要是由 `image-wrapper` 组成,它也是一个 `Custom-Component`,主要功能是: 335 | 336 | - 强制长宽 1:1 裁剪显示图片 337 | - 点击查看大图 338 | - 未加载完成时显示 加载中 339 | 340 | 具体代码这里就不展示了,比较简单,读者可以在 `component/image-wrapper` 里找到。 341 | 342 | 回过头看 `main-area` 的其他新增部分,细心的读者会发现有这么一句: 343 | 344 | ```html 345 | 无数据 348 | ``` 349 | 350 | 这会在 Feed 流暂时没有获取到数据时给用户一个提示。 351 | 352 | #### 3.2 collections: poster、poster_users 353 | 354 | 展示 Feed 流的部分已经编写完毕,现在就差实际数据了。根据上一小节 `poster-item` 的主要信息,我们可以初步推断出一条迷你微博在 [云数据库](https://developers.weixin.qq.com/miniprogram/dev/wxcloud/basis/capabilities.html#%E6%95%B0%E6%8D%AE%E5%BA%93) 的 collection `poster` 里是这样存储的: 355 | 356 | ```json 357 | { 358 | "username": "Tester", 359 | "date": "2019-07-22 12:00:00", 360 | "text": "Ceshiwenben", 361 | "photo": "xxx" 362 | } 363 | ``` 364 | 365 | 先来看 `username`。由于社交平台一般不会限制用户的昵称,所以如果每条迷你微博都存储昵称,那将来每次用户修改一次昵称,就要遍历数据库把所有迷你微博项都改一遍,相当耗费时间,所以我们不如存储一个 `userId`,并另外把 id 和 昵称 的对应关系存在另一个叫 `poster_users` 的 collection 里。 366 | 367 | ```json 368 | { 369 | "userId": "xxx", 370 | "name": "Tester", 371 | ...(其他用户信息) 372 | } 373 | ``` 374 | 375 | `userId` 从哪里拿呢?当然是通过之前已经授权的获取用户信息接口拿到了,详细操作之后会说到。 376 | 377 | 接下来是 `date`,这里最好是服务器时间(因为客户端传过来的时间可能会有误差),而云开发文档里也有提供相应的接口:[serverDate](https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-client-api/database/db.serverDate.html#db.serverDate)。这个数据可以直接被 `new Date()` 使用,可以理解为一个 UTC 时间。 378 | 379 | `text` 即文本信息,直接存储即可。 380 | 381 | `photo` 则表示附图数据,但是限于小程序 `image` 元素的实现,想要显示一张图片,要么提供该图片的 url,要么提供该图片在 [云存储](https://developers.weixin.qq.com/miniprogram/dev/wxcloud/basis/capabilities.html#%E5%AD%98%E5%82%A8) 的 id,所以这里最佳的实践是:先把图片上传到云存储里,然后把回调里的文件 id 作为数据存储。 382 | 383 | 综上所述,最后 `poster` 每一项的数据结构如下: 384 | 385 | ```json 386 | { 387 | "authorId": "xxx", 388 | "date": "utc-format-date", 389 | "text": "Ceshiwenben", 390 | "photoId": "yyy" 391 | } 392 | ``` 393 | 394 | 确定数据结构后,我们就可以开始往 collection 添加数据了。但是,在此之前,我们还缺少一个重要步骤。 395 | 396 | #### 3.3 用户信息录入 与 云数据库 397 | 398 | 没错,我们还没有在 `poster_users` 里添加一条新用户的信息。这个步骤一般在 `pages/circle/circle` 页面首次加载时判断即可: 399 | 400 | ```js 401 | getUserId: function(cb) { 402 | let that = this 403 | var value = this.data.userId || wx.getStorageSync("userId") 404 | if (value) { 405 | if (cb) { 406 | cb(value) 407 | } 408 | return value 409 | } 410 | wx.getSetting({ 411 | success(res) { 412 | if (res.authSetting["scope.userInfo"]) { 413 | wx.getUserInfo({ 414 | withCredentials: true, 415 | success: function(userData) { 416 | wx.setStorageSync("userId", userData.signature) 417 | that.setData({ 418 | userId: userData.signature 419 | }) 420 | db.collection("poster_users") 421 | .where({ 422 | userId: userData.signature 423 | }) 424 | .get() 425 | .then(searchResult => { 426 | if (searchResult.data.length === 0) { 427 | wx.showToast({ 428 | title: "新用户录入中" 429 | }) 430 | db.collection("poster_users") 431 | .add({ 432 | data: { 433 | userId: userData.signature, 434 | date: db.serverDate(), 435 | name: userData.userInfo.nickName, 436 | gender: userData.userInfo.gender 437 | } 438 | }) 439 | .then(res => { 440 | console.log(res) 441 | if (res.errMsg === "collection.add:ok") { 442 | wx.showToast({ 443 | title: "录入完成" 444 | }) 445 | if (cb) cb() 446 | } 447 | }) 448 | .catch(err => { 449 | wx.showToast({ 450 | title: "录入失败,请稍后重试", 451 | image: "/images/error.png" 452 | }) 453 | wx.navigateTo({ 454 | url: "/pages/index/index" 455 | }) 456 | }) 457 | } else { 458 | if (cb) cb() 459 | } 460 | }) 461 | } 462 | }) 463 | } else { 464 | wx.showToast({ 465 | title: "登陆失效,请重新授权登陆", 466 | image: "/images/error.png" 467 | }) 468 | wx.navigateTo({ 469 | url: "/pages/index/index" 470 | }) 471 | } 472 | } 473 | }) 474 | } 475 | ``` 476 | 477 | 代码实现比较复杂,整体思路是这样的: 478 | 479 | 1. 判断是否已存储了 `userId`,如果有直接返回并调用回调函数,如果没有继续 2 480 | 2. 通过 `wx.getSetting` 获取当前设置信息 481 | 3. 如果返回里有 `res.authSetting["scope.userInfo"]` 说明已经授权读取用户信息,继续 3,没有授权的话就跳转回首页重新授权 482 | 4. 调用 `wx.getUserInfo` 获取用户信息,成功后提取出 `signature`(这是每个微信用户的唯一签名),并调用 `wx.setStorageSync` 将其缓存 483 | 5. 调用 `db.collection().where().get()` ,判断返回的数据是否是空数组,如果不是说明该用户已经录入(注意 `where()` 中的筛选条件),如果是说明该用户是新用户,继续 5 484 | 6. 提示新用户录入中,同时调用 `db.collection().add()` 来添加用户信息,最后通过回调判断是否录入成功,并提示用户 485 | 486 | 不知不觉我们就使用了云开发中的 云数据库 功能,紧接着我们就要开始使用 云存储 和 云函数了! 487 | 488 | #### 3.4 addPoster 与 云存储 489 | 490 | 发送新的迷你微博,需要一个编辑新迷你微博的界面,路径我定为 `pages/circle/add-poster/add-poster`: 491 | 492 | ```html 493 | 494 | 495 | 496 |