【微信小程序】:后台管理系统(后端)笔记

2020/8/20 微信小程序

# 一、前期框架的搭建

后台采用的框架为Koa2框架,编程软件为vs code,node环境

  • 步骤一:创建一个空文件
  • 步骤二:在vs code打开该文件,并按ctrl+`打开终端
  • 步骤三:输入命令npm init(初始化package.json),也可以用npm init -y(直接默认初始化package.json) 在这里插入图片描述
  • 步骤四:输入命令npm install koa(下载koa) 在这里插入图片描述

# 二、获取AcessToken(接口调用凭证)

      获取小程序全局唯一后台接口调用凭据(access_token)。调用绝大多数后台接口时都需使用 access_token,开发者需要进行妥善保存。       由于,我是调用云函数,所以必须需要接口调用凭证。

注意: 在这里插入图片描述 中控服务器是自己搭建的后台

请求地址及参数 GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET

在这里插入图片描述 因为要不断使用接口调用凭证,所以将其封装成一个方法,再向外抛出接口。

代码实现:

  • 在实现前,因为需要向第三方请求数据(请求接口调用凭证),故需要下载第三方库,我使用的是request-promise(依赖于request)
  • 在终端输入先输入命令npm install request 下载成功后再输入命令npm install request-promise
  • 也可以使用axios第三方库进行请求数据
const rp = require('request-promise')
const fs = require('fs')//自带的
const path = require('path')//自带的
const fileName = path.resolve(__dirname, './access_token.json')//创建文件并获取绝对路径
const APPID = '您的小程序ID'
const APPSECRET = '您的小程序密钥'//不能泄露
const URL = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${APPID}&secret=${APPSECRET}`

//更新AccessToken
const updateAccessToken = async () => {
    //发送请求获取AccessToken
    const resStr = await rp(URL)
    const res = JSON.parse(resStr)
    
    //  将AccessToken写入文件
    if (res.access_token) {
    //JSON.stringify()将对象转化成字符串
        fs.writeFileSync(fileName, JSON.stringify(
            {
                access_token: res.access_token,
                createTime: new Date()
            }
        ))
    } else {//如果AccessToken不存在,再次更新AccessToken
        await updateAccessToken()
    }
}

//获取AccessToken
const getAccessToken = async () => {
    //读取文件获取AccessToken
    try {
    //读取文件,编码为utf-8
        const readRes = fs.readFileSync(fileName, 'utf8')
        const readobj = JSON.parse(readRes)
        const createTime=new Date(readobj.createTime).getTime()
        const nowTime=new Date().getTime()
       if((nowTime-createTime)/1000/60/60>=2){
       //判断是否已经超过俩个小时,超过俩个小时再次调用updateAccessToken方法
           await updateAccessToken()
       }
        return readobj.access_token
    } catch (error) {//文件不存在,说明AccessToken没有被更新
       await updateAccessToken()
       getAccessToken()
    }
}

// 定时器(定时获取AccessToken 提前5分钟)
setInterval(async () =>{
    await updateAccessToken()
},6900*1000)

//导出接口(给调用方使用)
module.exports=getAccessToken
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

运行测试

  • 在终端进入到该文件夹中(cd ..返回到上级目录或者cd 目标目录)
  • 输入命令编译文件 node 文件名 在这里插入图片描述

# 三、搭建后台,并返回数据到前端(重点)

前期准备 (1)由于我前端和后台都是在本地运行,但端口号不同,此时会产生跨域问题。

  • 解决方法: 我采用的方法时用cors来解决,首先在项目中打开终端,输入命令npm install koa2-cors 在这里插入图片描述 然后在项目的入口文件(app.js)中,导入该第三方库,解决跨域问题
const cors=require('koa2-cors')
//解决跨域问题
app.use(cors({
    //配置允许访问后台的URL地址(前端地址)
    origin:['http://localhost:9528'],
    credentials:true
}))
1
2
3
4
5
6
7

在实现前后端分离,都会出现跨域问题,有以下几种形式

  • http://www.a.com http://www.b.com 域名不一样
  • http://www.a.com:8080 http://www.a.com:8081 端口不一样
  • http://www.a.com http://news.a.com 主域和子域之间
  • http://www.a.com https://www.a.com 协议不一样

(2)配置后端路由,我使用的是koa-router,打开项目终端,输入命令npm install koa-router 在这里插入图片描述

(3)下载第三请求库request-promise,由于前面部分已经下载了,所以直接导入即可。

代码实现

知识点一:调用云函数
我这里是通过调用云函数实现从云数据库中获取歌单信息,下面是我部署的云函数(云函数名为music,路由名(地址)为playlist)
app.router('playlist', async (ctx, next) => {
    ctx.body = await cloud.database().collection('playlist')
      .skip(event.start)
      .limit(event.count)
      .orderBy('createTime', 'desc').get()
      .then((res) => {
        return res
      })
  })
1
2
3
4
5
6
7
8
9

后台调用云函数

const Router=require('koa-router')
const router=new Router()

const getAccessToken=require('../utils/getAccessToken.js')
const rp=require('request-promise')

// /list是后台路由地址
router.get('/list',async(ctx,next)=>{
    // 获取调用接口凭证
    const access_token= await getAccessToken()
    //云数据库运行环境
    const ENV='您的云开发环境ID'
    //调用云函数地址(参数name是您的云函数名字)
    const URL=`https://api.weixin.qq.com/tcb/invokecloudfunction?access_token=${access_token}&env=${ENV}&name=music`
    //获取前端的发送过来的请求数据
    const query=ctx.request.query
    //调用云函数的请求参数(封装成对象)
    const option={
        method:'POST',//必须是POST请求
        url:URL,
        body:{
           $url:'playlist',//云函数的路由名(地址)
           start:parseInt(query.start),
           count:parseInt(query.count)
        },
        json:true//返回格式为JSON格式
    };
    //发送请求
   const data=await rp(option).then((res)=>{
        //console.log(res)
        return JSON.parse(res.resp_data).data
    }).catch((err)=>{
        console.log(err)
    })
    //返回数据给前端
    ctx.body={data,code:20000}
})
module.exports=router//将路由抛出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

在返回数据中,一个数据code:20000 目的是:因为我前端采用的是vue-admin-template搭建的前端管理系统,必须返回code:20000。

优化代码 由于后台大部分功能是调用云函数,所以说调用云函数的代码内容基本是一样的。可以将调用云函数的方法进行封装,下面是封装的代码

const getAccessToken = require('./getAccessToken.js')
const rp = require('request-promise')

const callCloudFn = async (ctx, fnName, params) => {
    const ACCESS_TOKEN = await getAccessToken()
    const options = {
        method: 'POST',
        uri: `https://api.weixin.qq.com/tcb/invokecloudfunction?access_token=${ACCESS_TOKEN}&env=${ctx.state.env}&name=${fnName}`,
        //ctx.state.env通过全局中间件获取云开发环境,在入口文件(app.js)有配置
        body: {
            ...params
        },
        json: true 
    }

    return await rp(options)
        .then((res) => {
            return res//注:返回的格式是字符串
        })
        .catch(function (err) {
        })
}

module.exports = callCloudFn
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

此时需要修改路由list中的代码

const Router=require('koa-router')
const router=new Router()
const rp=require('request-promise')
const callCloudFun=require('../utils/callCloudFn')

router.get('/list',async(ctx,next)=>{
    //获取前端请求的数据
    const query=ctx.request.query
    //发送请求
    const res=await callCloudFun(ctx,'music',{
        $url:'playlist',
        start:parseInt(query.start),
        count:parseInt(query.count)
    })
    let data=[]
    if(res.resp_data){
        data=JSON.parse(res.resp_data).data
    }
    ctx.body={
        data,
        code:20000,
    }
})
module.exports=router
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

这样的好处就是减少代码的重复率,增加代码的利用率。

知识点二:API调用云数据库

前期准备 (1)下载koa-body,获取post请求数据 在这里插入图片描述 在app.js配置以下代码

//接收post参数解析
app.use(koaBody({
    multipart: true,
}))
1
2
3
4

(2)创建功能模块

const rp=require('request-promise')
const getAccessToken = require('./getAccessToken.js')
const callCloudDB=async(ctx,fnName,query={})=>{
//fnName数据操作的类型 query数据操作语句
     const access_token=await getAccessToken()
     const options={
         method:'POST',
         uri:`https://api.weixin.qq.com/tcb/${fnName}?access_token=${access_token}`,
         body:{
             query,
             env:ctx.state.env,
         },
         json:true
     }
     return await rp(options).then((res)=>{
           return res
     }).catch((err)=>{
         console.log(err)
     })
}
module.exports=callCloudDB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

(3)更新数据 因为前端提交更新数据的形式是post,所以后端代码应该为post

router.post('/updatePlaylist',async(ctx,next)=>{
    //获取请求体
    const params=ctx.request.body
    //更新语句
    const query=`db.collection('playlist')
                 .doc('${params._id}').update({
                     data:{
                         name:'${params.name}',
                         copywriter:'${params.copywriter}'
                     }
                 })
                 `
     const res= await callCloudDB(ctx,'databaseupdate',query)
     ctx.body={
         code:20000,
         data:res
     }
})
module.exports=router
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

(4)删除数据

router.get('/deletePlaylist',async(ctx,next)=>{
    const query=`db.collection('playlist').doc('${ctx.request.query.id}').remove()`
    const res=await callCloudDB(ctx,'databasedelete',query)
    ctx.body={
        code:20000,
        data:res
    }
})
1
2
3
4
5
6
7
8

(5)增加数据

 const query=`db.collection('swiper').add({
        data:{
            fileId:'${fileid}'
        }
    })`
    const res=await callCloudDB(ctx,'databaseadd',query)
1
2
3
4
5
6

(6)查询数据

router.get('/getById',async(ctx,next)=>{
    const query=`db.collection('playlist').doc('${ctx.request.query.id}').get()`
    const res=await callCloudDB(ctx,'databasequery',query)
    //console.log(res)
    ctx.body={
        code:20000,
        data:JSON.parse(res.data)
    }
})
1
2
3
4
5
6
7
8
9
知识点三:文件的上传、下载和删除

创建功能模块

const getAccessToken = require('./getAccessToken.js')
const callCloudDB = require('./callCloudDB.js')

const rp =require('request-promise')
const fs=require('fs')

const cloudStore={

    //下载图片
    async download(ctx,fileList){
        const ACCESSTOKEN=await getAccessToken()
        const options={
            method:'POST',
            uri:`https://api.weixin.qq.com/tcb/batchdownloadfile?access_token=${ACCESSTOKEN}`,
            body:{
               env:ctx.state.env,
               file_list:fileList
            },
            json:true
        }
        return await rp(options).then((res)=>{
              return res
        }).catch((err)=>{
            console.log(err)
        })
    },
    
    //上传图片到云存储
    async uploadfile(ctx){
        //步骤一:发送上传文件的请求,获取相应信息
        const ACCESSTOKEN=await getAccessToken()
        const file=ctx.request.files.file//上传文件信息
        //文件路径(随机)
        const path=`swiper/${Date.now()}-${Math.random}-${file.name}`
        const options={
            method:'POST',
            uri:`https://api.weixin.qq.com/tcb/uploadfile?access_token=${ACCESSTOKEN}`,
            body:{
                env:ctx.state.env,
                path:path
            },
            json:true
        }
        const info= await rp(options).then((res)=>{
            return res
        }).catch((err)=>{
            console.log(err)
        })
        //console.log(info)
        
        //步骤二:上传图片
        const params={
            method:"POST",
            headers:{
                "content-type":'multipart/form-data',                
            },
            uri:info.url,
            formData:{
                key:path,
                Signature:info.authorization,
                'x-cos-security-token':info.token,
                'x-cos-meta-fileid':info.cos_file_id,
                file:fs.createReadStream(file.path)//二进制文件
            },
            json:true
        }
         await rp(params)
         return info.file_id//返回文件id到调用方,将其存储在云数据库
    }

    //删除文件
    async delFile(ctx,fileid_list){
        const ACCESSTOKEN=await getAccessToken()
        const options={
            method:'POST',
            uri:` https://api.weixin.qq.com/tcb/batchdeletefile?access_token=${ACCESSTOKEN}`,
            body:{
                fileid_list,
                env:ctx.state.env,
                
            },
            json:true
        }
        return await rp(options).then((res)=>{
            return res
        }).catch((err)=>{
            console.log(err)
        })
    }
}

module.exports= cloudStore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93

(2)调用功能模板

//读取图片(获取歌曲链接)
router.get('/list',async(ctx,next)=>{
   //读取数据库文件信息
   const query=`db.collection('swiper').get()`
   const res=await callCloudDB(ctx,'databasequery',query)
   
   //获取图片链接(http形式)
   let fileList=[]
   const data=res.data
   //遍历文件信息,封装请求对象
   for(let i=0,len=data.length;i<len;i++){
       fileList.push({
           fileid:JSON.parse(data[i]).fileId,
           max_age:7200
       })
   }
   //获取下载链接(http形式)
   const dlRes=await cloudStore.download(ctx,fileList)
   
   //封装对象,将所需要的信息返回到前端
   let returnData=[]
   for(let i=0,len=dlRes.file_list.length;i<len;i++){
         returnData.push({
            download_url:dlRes.file_list[i].download_url,//文件下载链接
            fileId : dlRes.file_list[i].fileid,
            _id:JSON.parse(data[i])._id
        })
   }
   ctx.body={
       code:20000,
       data:returnData
   }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//上传图片
router.post('/upload',async(ctx,next)=>{
    //上传图片 返回文件id
    const fileid=await cloudStore.uploadfile(ctx)
    //console.log(fileid)
    const query=`db.collection('swiper').add({
        data:{
            fileId:'${fileid}'
        }
    })`
    //写入数据库
    const res=await callCloudDB(ctx,'databaseadd',query)
    ctx.body={
        code:20000,
        id_list:res.id_list
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//删除文件
export function del(params){
    return request({
        params,
        url:`${baseURL}/swiper/del`,
        method:'get'
    })
}
1
2
3
4
5
6
7
8

# 四、配置入口文件(app.js)

const Koa=require('koa')
const Router=require('koa-router')
const cors=require('koa2-cors')
const koaBody=require('koa-body')
const ENV='haocloud-xagb6'
const app=new Koa()
const router=new Router()

//解决跨域问题
app.use(cors({
    //配置允许访问后台的URL地址
    origin:['http://localhost:9528'],
    credentials:true
}))

//接收post参数解析
app.use(koaBody({
    multipart: true,
}))

//全局中间键
app.use(async(ctx,next)=>{
    ctx.state.env=ENV
    await next()
})

// http://localhost:3000/playlist/list(调用获取歌单的URL)
const playlist=require('./controller/playlist.js')
router.use('/playlist',playlist.routes())

const swiperList=require('./controller/swiper.js')
router.use('/swiper',swiperList.routes())

const blogList=require('./controller/blog.js')
router.use('/blog',blogList.routes())

//声明router
app.use(router.routes())
//允许router下的所有方法
app.use(router.allowedMethods())



app.listen(8080,()=>{
    console.log('服务开启在端口为8080')
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

此时,如果前端要获取歌单信息,则需要向后端地址为http://localhost:8080/playlist/list发送请求

  • playlist指的是 routers.use('/playlist',playlist.routes())中的playlist
  • list指的是router.get('/list',async(ctx,next)=>{}中的list(就是一个叫playlist的router的路由下的名为list的路由 个人理解)

最后启动后端服务,在终端输入命令 node app.js(编译入口文件) 结束编译:ctrl+C即可