Fork me on GitHub
余鸢

微信公众号开发(八):后台商品数据系列操作及七牛云图片上传

封装API

通过mongoose拼接查询语句获取数据,将它们统一封装在一个API文件中,需要哪些数据调用想用的方法即可。

server/api/wiki.js

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
import mongoose from 'mongoose'
const WikiHouse = mongoose.model('WikiHouse')
const WikiCharacter = mongoose.model('WikiCharacter')
export async function getHouses () {
const data = await WikiHouse
.find({})
.populate({
path: 'swornMembers.character',
select: '_id name cname profile'
})
.exec()
return data
}
export async function getHouse (_id) {
const data = await WikiHouse
.findOne({_id: _id})
.populate({
path: 'swornMembers.character',
select: '_id name profile cname nmId'
})
.exec()
return data
}
export async function getCharacters (limit = 20) {
const data = await WikiCharacter
.find({})
.limit(Number(limit))
.exec()
return data
}
export async function getCharacter (_id) {
const data = await WikiCharacter
.findOne({_id: _id})
.exec()
return data
}

获取家族数据

server/routers/wiki.js

1
2
3
4
5
6
7
8
9
10
11
12
13
@controller('/wiki')
export class WechatController {
// 获取家族数据
@get('/houses')
async getHouses (ctx, next) {
const data = await api.wiki.getHouses()
ctx.body = {
data: data,
sucess: true
}
}
}

获取家族详细资料

1
2
3
4
5
6
7
8
9
10
11
@get('/houses/:_id')
async getHouse (ctx, next) {
const { params } = ctx
const { _id } = params
if (!_id) return (ctx.body = {sucess: false, err: '_id is required'})
const data = await api.wiki.getHouse(_id)
ctx.body = {
data: data,
sucess: true
}
}

获取主要成员数据

1
2
3
4
5
6
7
8
9
@get('/characters')
async getCharacters (ctx, next) {
let { limit = 20 } = ctx.query
const data = await api.wiki.getCharacters(limit)
ctx.body = {
data: data,
sucess: true
}
}

获取成员的个人详情

1
2
3
4
5
6
7
8
9
10
11
@get('/characters/:_id')
async getCharacter (ctx, next) {
const { params } = ctx
const { _id } = params
if (!_id) return (ctx.body = {sucess: false, err: '_id is required'})
const data = await getCharacter(_id)
ctx.body = {
data: data,
sucess: true
}
}

修改数据请求地址

store/serice.js

1
2
3
fetchCharacters() {
return axios.get(`${baseUrl}/wiki/characters`)
}

修改页面

在页面中添加img,用拼接的方式获取七牛云上的图片地址。

pages/index.vue

1
2
.house-flag
img(:src='imageCDN + item.name + ".jpg"')

state中定义imageCDN

1
imageCDN: 'http://omux103p0.bkt.clouddn.com/',

记得修改入口文件的执行文件路径

start.js

1
require('./server')

家族详情页

点击家族名称跳转到家族详情页,数据houses已经存在,只需要再完善展示效果,添加img

1
2
3
4
5
6
7
8
.house-media
img(v-if='house.name' :src='imageCDN + house.name + ".jpg"')
.title 主要角色
.body(v-for='(item, index) in house.swornMembers' :key="index")
.members(v-if='item.character')
img(:src='item.character.profile' @click='showCharacter(item)')

个人详情页和家族页一样,只需要修改img图片地址即可,这里就不贴代码了。

商城路由

商品建模

使用mongoose

server/database/schema/product.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const mongoose = require('mongoose')
const { Schema } = mongoose
const Mixed = Schema.Types.Mixed
const ProductSchema = new mongoose.Schema({
price: String,
title: String,
intro: String,
images: [String],
parameters: [
{
key: String,
value: String
}
]
})
mongoose.model('Product', ProductSchema)

商品操作的API

server/api/product.js

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
import mongoose from 'mongoose'
const Product = mongoose.model('Product')
export async function findProduct(_id) {
const data = await Product
.find({_id: _id})
.exec()
return data
}
export async function getProducts(limit = 50) {
const data = await Product
.find({})
.limit(Number(limit))
.exec()
return data
}
export async function getProduct(_id) {
const data = await Product
.findOne({_id: _id})
.exec()
return data
}
export async function save(product) {
product = new Product(product)
product = await product.save()
return product
}
export async function update(product) {
product = await product.save()
return product
}
export async function del(product) {
console.log(product)
try {
await product.remove()
} catch (e) {
e
}
return true
}

商品路由

增加商品路由,包括增删改查系列商品操作

server/routers/product.js

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import api from '../api'
import { controller, get, post } from '../decorator/router'
import xxs from 'xxs'
import R from 'ramda'
@controller('/api')
export class ProductController {
@get('/products')
async getProducts (ctx, next) {
let { limit = 50 } = ctx.query
const data = await api.product.getProducts(limit)
ctx.body = {
data: data,
sucess: true
}
}
@get('/products/:_id')
async getProduct (ctx, next) {
const { params } = ctx
const { _id } = params
if (!_id) return (ctx.body = {sucess: false, err: '_id is required'})
const data = await api.product.getProduct(_id)
ctx.body = {
data: data,
sucess: true
}
}
@post('/products')
async postProducts (ctx, next) {
let product = ctx.request.body
console.log(ctx.request)
product = {
title: xxs(product.title),
price: xxs(product.price),
intro: xxs(product.intro),
images: R.map(xxs)(product.images),
parameters: R.map(
item => ({
key: xxs(item.key),
value: xxs(item.value)
})
)(product.parameters)
}
try {
product = await api.product.save(product)
ctx.body = {
success: true,
data: product
}
} catch (e) {
ctx.body = {
success: false,
err: e
}
}
}
@put('/products')
async getCharacter (ctx, next) {
let body = ctx.request.body
const { _id } = body
if (!_id) {
return (ctx.body = {success: false, err: '_id is required'})
}
let product = await api.product.getProduct(_id)
if(!product) {
return (ctx.body = {
success: false,
err: 'product not exist'
})
}
product.title = xxs(body.title)
product.price = xxs(body.price)
product.intro = xxs(body.intro)
product.images = R.map(xxs)(body.images)
product.parameters = R.map(
item => ({
key: xxs(item.key),
value: xxs(item.value)
})
)(product.parameters)
try {
product = await api.product.update(product)
ctx.body = {
success: true,
data: product
}
} catch (e) {
ctx.body = {
success: false,
err: e
}
}
}
@del('products/:id')
async delProducts (ctx, next) {
const params = ctx.params
const {_id} = params
if (!_id) return (ctx.body = {success: false, err: '_id is required'})
let product = await api.product.getProduct(_id)
if (!product) return (ctx.body = {success: false, err: 'product is required'})
try {
product = await api.product.del(product)
ctx.body = {
success: true
}
} catch (e) {
ctx.body = {
success: false,
err: e
}
}
}
}

设置暴露路由,修改文件

server/api/index.js

1
2
3
4
5
export default {
wechat: wechat,
wiki: wiki,
product: product
}

后台商品页

pages/admin/products.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template lang="pug">
.content
.related-products
...
.edit-product(:class='{active: editing}')
...
.edit-footer
button.btn.save(@click='saveEdited', v-if='!isProduct') 创建宝贝
button.btn.save(@click='saveEdited', v-if='isProduct') 保存修改
.btn.add-parameter(@click='addParameter')
.material-icon add
| 添加参数
.float-btn(@click='createdProduct')
.material-icon add
v-snackbar(:open.sync='openSnackbar')
span(slot='body') 保存成功
</template>

页面具体代码不细贴了

定义数据

1
2
3
4
5
6
7
8
9
10
11
data() {
return {
isProduct: false,
openSnackbar: false,
edited: {
images: [],
parameters: []
},
editing: false
}
},

isProduct:是否有product数据

openSnackbar:打开openSnackbar

edited:编辑数据

editing:编辑状态

引入products

1
2
3
4
5
6
computed: {
...mapState([
'imageCDN',
'products'
])
},

获取数据

1
2
3
async created() {
this.$store.dispatch('fetchProducts')
},

编辑intro

1
2
3
4
5
editedIntro(e) {
let html = e.target.value
html = html.replace(/\n/g, '<br />')
this.edited.intro = html
},

编辑product

点击编辑按钮,触发editProduct(item)方法

1
2
3
4
5
editProduct(item) {
this.edited = item
this.isProduct = true
this.editing = true
},

创建product

点击添加按钮,弹出创建数据窗户,写入edited参数,设置状态

1
2
3
4
5
6
7
8
createProduct() {
this.edited = {
images: [],
parameters: []
}
this.isProduct = false
this.editing = true
},

保存product

点击创建宝贝或者点击保存修改触发saveEdited()事件

1
2
3
4
5
6
7
8
9
10
11
12
async saveEdited() {
this.isProduct
? await this.$store.dispatch('putProduct', this.edited)
: await this.$store.dispatch('saveProduct', this.edited)
this.openSnackbar = true
this.isProduct = false
this.edited = {
images: [],
parameters: []
}
this.editing = !this.editing
},

删除product

1
2
3
async deleteProduct(item) {
await this.$store.dispatch('deleteProduct', item)
},

添加参数

创建product时,添加参数项,或者删除某个参数项

1
2
3
4
5
6
7
8
9
addParameter() {
this.edited.parameters.push({
key: '',
value: ''
})
},
removeParameter(index) {
this.edited.parameters.splice(index, 1)
}

数据增删改查

store/actions.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async saveProduct({ state, dispatch }, product) {
await axios.post('/api/products', product)
let res = await dispatch('fetchProducts')
return res.data.data
},
async putProduct({ state, dispatch }, product) {
await axios.put('/api/products', product)
let res = await dispatch('fetchProducts')
return res.data.data
},
async deleteProduct({ state, dispatch }, product) {
await axios.delete(`/api/products/${product._id}`)
let res = await dispatch('fetchProducts')
return res.data.data
},

修改请求地址

store/service.js

1
2
3
4
5
6
fetchProducts() {
return axios.get(`${baseUrl}/api/products`)
}
fetchProduct(id) {
return axios.get(`${baseUrl}/api/products/${id}`)
}

解析body数据

存储商品时是post请求,body需要解析。通过MIDDLEWARES引入中间件,添加common中间件,

server/index.js

1
const MIDDLEWARES = ['database', 'common', 'router']

使用koa-bodyparse解析数据

server/middlewares/common.js

1
2
3
4
5
import koaBody from 'koa-bodyparser'
export const addBody = app => {
app.use(koaBody())
}

上传图片到七牛云

这次是从浏览器端上传图片,先从服务器上拿到授权token,结合token去完成在客户端生成的图片(如果没有token的话,任何人都可以往你的服务器传图片,不安全)。然后再调用js-sdk完成上传图片功能。这里需要引入两个组件:

random-token:随机生成key

qiniu-web-uploader:上传图片组件

1
2
import randomToken from 'random-token'
import Uploader from 'qiniu-web-uploader'

页面中添加上传图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.input-group
label 图片
.upload-images
.img(v-for='item, index in edited.images')
img(:src='imageCDN + item + "?imageView2/1/format/jpg/q/75/imageslim"')
.tools
.material-icon(@click='deleteImg(index)') delete
.upload-btn
g#Page-1(stroke='none', stroke-width='1', fill='none', fill-rule='evenodd')
g#ic_backup_black_24px(transform='translate(-1.000000, -6.000000)')
polygon#Shape(points='0 0 55 0 55 55 0 55')
path#outline(d='...')
path#Shape(d='...', fill='#CFD8DC', fill-rule='nonzero')
br
.text 上传图片
input(type='file', @change='uploadImg($event)')

创建点击上传图片事件@change='uploadImg($event)'。传入事件e,声明点击事件元素file,随机生成的文件名key,调用getUptoken(key)获取token值,创建Uploader实例,传入file和token值,监听上传进度,调用上传图片函数uploader.upload(),最后将获取到的可以放入edited.images

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
async getUptoken(key) {
let res = await axios.get('/qiniu/token', {
params: {
key: key
}
})
return res.data.data.token
},
async uploadImg(e) {
let file = e.target.file[0]
let key = randomToken(32)
key = `products/${key}`
let token = await this.getUptoken(key)
let uptoken = {
uptoken: token,
key: Buffer.from(key).toString('base64')
}
// Uploader.QINIU_UPLOAD_URL = '//up-z2.qiniu.com'
let uploader = new Uploader(file, uptoken)
uploader.on('progress', () => {
console.log(uploader.percent)
// let dashoffset = this.upload.dasharray * (1 - uploader.percent)
// this.upload.dashoffset = dashoffset
})
let res = await uploader.upload()
uploader.cancel()
console.log(res)
this.edited.images.push(res.key)
},

问题:

这里会报error: incorrect region, please use up-z2.qiniu.com的错误

解决:

是因为空间是华南的,这里我直接换用华东空间就可以正常使用了。

修改server/lib/qiniu.js

1
2
3
4
5
6
7
8
9
10
11
12
const bucket2 = 'vueapp'
const ak = config.qiniu.AK
const sk = config.qiniu.SK
const mac = new qiniu.auth.digest.Mac(ak, sk)
let options = {
scope: bucket2
}
export const uptoken = (key) => {
let putPolicy = new qiniu.rs.PutPolicy(options)
let uploadToken = putPolicy.uploadToken(mac)
return uploadToken
}

添加七牛路由

server/routes/qiniu.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { controller, get, post, put } from '../decorator/router'
import { uptoken } from '../lib/qiniu'
@controller('/qiniu')
export class QIniuController {
@get('token')
async qiniuToken (ctx, next) {
let key = ctx.query.key
let token = uptoken(key)
ctx.body = {
success: true,
data: {
key: key,
token: token
}
}
}
}

上传图片测试成功,如图:

wx47

前台页面展示

和之前一样,只需要修改图片img地址即可,需要修改的页面分别是pages/shopping.vuepages/deal.vue两个页面,代码很简单这里就不做展示了,具体看源码即可。