# multilingual_shop_node **Repository Path**: iiiboy/multilingual_shop_node ## Basic Information - **Project Name**: multilingual_shop_node - **Description**: 多语言电商平台后端代码(不包括数据库数据),主要使用了node+koa框架进行开发,使用jwt进行验证,mongoose进行数据库管理 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-03-10 - **Last Updated**: 2021-03-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 多语言电商项目后端开发笔记 mongodb b站学习: mongoose b站学习: mongodb find与findOne详解: ## 1. 技术栈 1. node 2. koa 3. mongodb ## 2. 搭建环境 1. `yarn init`,`git init` 2. `yarn add koa koa-router mongoose` 然后新建index.js进行配置 ```js const Koa = require('koa') const Router = require('koa-router') const mongoose = require('mongoose') // 实例化koa const app = new Koa() const router = new Router() // 启用routes 与express的路由有点类似 app.use(router.routes).use(router.allowedMethods) // 这里使用config下的keys存储对应的信息 const db = require('./src/config/keys') // 连接数据库 mongoose.connect( `mongodb://${db.name}:${db.pwd}@localhost:${db.port}/${db.database}` , { useNewUrlParser: true, useUnifiedTopology: true } ).then(() => { console.log('数据库连接成功') }).catch((err) => { console.log(err) }) const port = process.env.port || 5000 app.listen(port, () => { console.log(`监听${port}端口`) }) ``` [comment]: <> "3. 对于post请求参数的解析,在koa中需要`yarn add koa-bodyparser` 这个依赖,然后可以在 ctx.request.body中访问到对应的参数" [comment]: <> " 记得在index.js中进行使用 `const bodyParser = require('koa-bodyparser'); app.use(bodyParser());`" 3. 对于post请求参数的解析,开始使用的是koa-bodyparser,但是由于需要上传图片,需要koa-multer依赖,不过 koa-multer 和 koa-route(注意不是 koa-router) 存在不兼容的问题。,所以使用koa-body代替这两个依赖, `app.use(body({ multipart: true //支持文件上传 }))` ## 3. 使用路由管理 koa-router的使用方式主要为 ```js const Router = require('koa-router') // 引入mongoose的model const User = require('../../models/user') // 封装了加密密码和检查的方法 const toolBcrypt = require('../../config/toolBcrypt') // 用于查询全球通用头像 const gravatar = require('gravatar') const router = new Router() // 可以链式调用 也就是 router.post('/1').post('/2').get('3') // 注意 router可以传入中间件 比如 router.get('/1',()=>{},async ctx=>{}) 中间哪个函数就是一个中间件,可以传入多个中间件 router.post('/register', async ctx => { if (ctx.request.body) { const { body } = ctx.request const findRes = await User.find({ email: body.email }) if (findRes.length) { ctx.status = 500 ctx.body = { msg: "该用户已经存在" } } else { // 这里获取全球通用头像 https://www.npmjs.com/package/gravatar // 第一个参数是邮箱地址 第二个参数是选项 s代表大小 r不知道 d表示默认 如果查询不到就为默认 具体的看网址 const avatar = gravatar.url(body.email, { s: '200', r: 'pg', d: 'mm' }) // 这里创建新用户 const newUser = new User({ ...body, password: toolBcrypt.enCrypt(body.password), avatar }) console.log(newUser) await newUser.save().then((res) => { console.log(res, '创建成功') ctx.status = 200 ctx.body = newUser }).catch((err) => { console.log(err, '创建失败') ctx.status = 500 ctx.body = { msg: "注册失败" } }) } } else { ctx.status = 403 ctx.body = { msg: "请传入数据" } } }) module.exports = router.routes() ``` ## 4. 使用mongoose mongoose的官方文档 1. 使用mongoose连接 ```js // 连接数据库 const mongoose = require('mongoose') // 连接数据库 const db = require('./keys').mongoURI // 对发处的警告进行设置 mongoose.set('useCreateIndex', true) mongoose.connect( `mongodb://${db.name}:${db.pwd}@localhost:${db.port}/${db.database}` , { useNewUrlParser: true, useUnifiedTopology: true } ).then(() => { console.log('数据库连接成功') }).catch((err) => { console.log(err) }) ``` 2. 使用mongoose创建表格,并且对表格进行管理 ```js const mongoose = require('mongoose') const User = new mongoose.Schema({ // !注意这里 之前是account 但是这里我修改成了username 如果在还是account的时候就创建了users这张表的话 那么这里修改后 // !应该删除users这张表 然后重新创建 不然新建user的话会报错 因为虽然插入进去的account已经变为username了 但是已经创建了 其实还是account // !如果使用了unique 那么一定会报错 username: { type: String, required: true, trim: true }, password: { type: String, required: true, minlength: 6, trim: true }, email: { type: String, required: true, trim: true, unique: true, match: /^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/ }, avatar: String, data: { type: Date, default: Date.now() } }) module.exports = mongoose.model('users', User) ``` ## 5. 使用bcrypt给密码加密,使用gravatar查看全球通用头像 1. 安装依赖 bcrypt: `yarn add bcrypt` gravatar: `yarn add gravatar` 2. 导入并且进行使用 ```js // 用于加密密码 const bcrypt = require('bcryptjs') const toolBcrypt = { // 加密方法 enCrypt (code) { // 这里对password进行加密 视频中使用异步的方法 我这里试一下同步的方法 // 感觉都差不多 https://www.npmjs.com/package/bcryptjs npm网址 检查密码的方法在里面 // 这个创建的什么我不知道 但是可以用于加密密码 const salt = bcrypt.genSaltSync(8) // 还可以这样 不适用salt 第二个参数直接传入一个数字代替salt const hash = bcrypt.hashSync(code, salt) return hash }, // 检查方法 传入两个参数 code为加密前的字符串 hash为加密后的字符串 checkCrypt (code, hash) { return bcrypt.compareSync(code, hash); } } module.exports = toolBcrypt ``` ```js const gravatar = require('gravatar') // 这里获取全球通用头像 https://www.npmjs.com/package/gravatar // 第一个参数是邮箱地址 第二个参数是选项 s代表大小 r不知道 d表示默认 如果查询不到就为默认 具体的看网址 const avatar = gravatar.url(body.email, { s: '200', r: 'pg', d: 'mm' }) ``` ## *6. 使用jwt* npm-jwt: 阮一峰jwt入门: 1. 安装依赖 `yarn add jsonwebtoken` 2. 在需要返回token的地方进行引入 ```js // 引入 const jwt = require('jsonwebtoken') // 生成token // sign方法可以传入四个参数 前两个参数是必需的 // 第一个是负载 // 第二个是密钥或者私钥辅导生成token,一般这个参数都需要使用一个变量集中保存,使用时引入就是 // 第三个参数是选项 比如过期的时间 加密使用的算法等 具体看npm-jwt 第四个参数是回调函数 const token = jwt.sign(payload, secretOrPrivateKey, options, callback) ``` 3. 对生成的token进行返回 ## 7. 对比检查token 视频中进行token验证的依赖就是下面两个,但是在网上找到了koa-jwt,貌似用于验证token更加方便,所以这里使用koa-jwt来进行验证 参考网址: 这个文档说明了jwt与koa-jwt的使用方式,还解释了jwt.sign()几个参数的含义 参考网址: 这个文档中使用了jwt-simple 貌似比jwt使用更简便 对这两个依赖进行了注释,可能会用到,功能可能要多一些 1. 安装依赖`yarn add koa-jwt` 2. 局部鉴权 ```js // 这里进行引入 const koaJwt = require('koa-jwt') // 这里在路由中进行使用, 直接传入使用中间件 router.get( '/current', koaJwt({ secret: secretKey }), async ctx => { ctx.status = 200 ctx.body = { msg: '验证成功' } } ) ``` 3. 全局鉴权 ```js // 这个依赖进行验证token进行鉴权 const koaJwt = require('koa-jwt') // 这里就是当token报错的时候进行处理 当token验证失败时就会触发这个函数 app.use((ctx, next) => { return next().catch((err) => { if (401 == err.status) { ctx.status = 401; console.log('token验证失败') ctx.body = 'Protected resource, use Authorization header to get access\n'; } else { throw err; } }); }); // 使用koa-jwt全局鉴权 这个插件也必须放在app使用路由之前 app.use( koaJwt({ secret: secretKey }).unless({ path: [/^(?!\/register)/, /^(?!\/login)/] }) ) ``` ## 8. 解决跨域问题 现在项目中一般都是使用cors来解决跨域问题的: 因为用户们访问的都是服务器上的网页,所以我们后端直接允许服务器的origin,这样一来所有的请求都可以请求到,但是这个时候配合token一起使用,对于公开的接口可以直接进行请求,但是私密的接口需要验证token,如果验证不通过也就不能进行请求!! 1. 安装koa-cors `yarn add @koa/cors` 参考地址: 2. 在入口文件中进行引入并且使用该依赖 这个依赖需要判断每一个请求 所以应该放在最前面 ```js // 导入cors解决跨域的问题 const cors = require('koa-cors') // 导入cors的选项 const corsOptions = require('./src/config/cor') app.use(cors(corsOptions)) ``` 3. 设置并导出koa-cors的配置选项 参考网址: 参考网址: ```js // 这个文件对koa-cors进行选项设置 module.exports = { // 这个用于设置允许跨域的origin 可以是一个函数 也可以是字符串 origin: (ctx) => { console.log(ctx.url) return '*' }, // 这里是允许的方法 如果只是设置了origin那么只会允许简单的请求方法 也就是 get head post // 如果使用put 或者 delete 或者 contentType:application/json 还是会被拦截下来,这个时候 // 就需要设置方法和头部了 而且 使用put和delete方法时浏览器会进行预检也就是发出请求之前浏览器 // 先发出一个options请求,通过了才会继续 所以在开发中经常会遇到发送两个请求一个是options请求 allowMethods: ['GET', 'POST', 'PUT', 'DELETE'], // 这里是允许的头部 cor默认允许的不包括这些 可以参考网址 // http://www.ruanyifeng.com/blog/2016/04/cors.html // 这里的Authorization 主要是为了通过token allowHeaders: ['Content-Type', 'Authorization', 'Accept'], // 这个选项让服务器允许将浏览器的头放在白名单内 这里主要针对浏览器自定义的头部 // 比如Authorization,这个必须现在这里允许后才能放在allowHeaders里 而且在mdn上译者标注 // 在跨源访问时如果服务器给浏览器返回了一些自定义的头 浏览器也是接收不了的需要在这里进行设置 exposeHeaders: ['Authorization'], // 这个选项设置预检的请求也就是put与delete请求的结果能够被缓存多久 单位为秒 maxAge: 5, // 在跨域过程中,如果浏览器向服务器的请求中如果要携带cookie就必须要设置withCredentials:true // 当设置withCredentials属性后,跨域请求的origin就不能为"*"了必须指定一个域名, 而且必须设置 // Access-Control-Allow-Credentials:true 服务器端才能将响应发送给浏览器端如果没有设置 将不能发送 // todo 具体的还不太了解 credentials: true, //默认为false // 这个是中间件本身的 意义为: 如果抛出错误则将setHeader添加到err.Header中 keepHeadersOnError: true } ``` ## 9. 对token进行解码,进行业务逻辑操作 ```js router.get( '/current', // 这里中间件进行token验证 koaJwt({ secret: secretKey }), async ctx => { // 这里当验证通过时 就取得token 取得token时需要去掉'Bearer ' const token = ctx.header.authorization.slice(7) console.log(token) // 然后在这里进行解码 const payload = jwt_verify(token, secretKey) // 这里要加await 而且mongoose默认的id键名是 _id const user = await User.findOne({ _id: payload.id }) console.log(payload, user) ctx.body = { success: true, data: user } }) ``` ## 10. 搭建静态资源服务器 目前要进行商品数据表格的搭建,因为商品少不了图片 所以需要先搭建一个静态资源服务器, 参考网址: 1. 安装koa-static `yarn add koa-static` 2. 在入口文件中进行引入配置,并且使用 ```js const serve = require('koa-static') const public = serve(path.join(___dirname, '/public/')) app.use(public) ``` ## 11. mongodb模糊查询,分页查询 参考地址:https://blog.csdn.net/yaomingyang/article/details/75103480 官方文档:https://docs.mongodb.com/manual/reference/operator/query/nor/ 进行分页搜索的主要就在这里 Model.find().skip(条数*(页码-1)).limit(条数) 下面是模糊查询的一些操作符 1. $regex:mongodb中进行模糊查询就在find里面添加参数就是了 $regex 表示正则匹配 使用 // 也可以 但是这里query是变量名所以不能使用/query/ 不然就是匹配包含'query' ; `find({zhTitle:{ $regex: query }})` 就表示zhTitle字段中有query这个变量的字符串的结果 2. $or find({$or:[]}) or就是或的意思 传入一个数组,里面包含一个或多个查询条件 比如`$or: [{ zhPrice: 2999 }, { enPrice: { $lt: 450 } }]` 就查询zhPrice为2999的或者enPrice字段小于450的 3. $in` find({quantity:{$in:[20,50]}})` 表示查询quantity字段等于20或者等于50的值 4. $and `find( { $and: [ { price: { $ne: 1.99}}, { price:{ $exists: true } } ] } )` $and表示且 这里表示查询price字段不等于1.99,而且price字段存在的结果 $and有隐式执行 比如 find({price:{$ne:1.99,$exists:true}}) 与上面是一样的 或者 find({count:1,num: 2}) 表示匹配count=1而且num=2的结果 5. $not 表示非 比如`find( { price: { $not: { $gt: 1.99 } } } )` 表示匹配price字段大于等于1.99或者price不存在的结果,注意 $not操作符不支持$regex正则表达式操作,但是可以使用//表示正则表达式 6. $nor 这个操作符相当于是$not 与 $or 的结合 `find( { $nor: [ { price: 1.99 }, { qty: { $lt: 20 } }, { sale: true } ] }` 表示匹配price不等于1.99,或qty大于等于20,或sale不为true的情况 如果price,qty,sale不存在也是包括的 7. $lt 小于 8. $gt 大于 9. $lte 小于等于 10. $gte 大于等于 11. $ne 不等于 12. $exists 存在 ## 12. mongodb多表联合查询 ```js router.get('/detail', async ctx => { const { gid } = ctx.query // !注意 这里实可以这样的await返回一个promise然后再then 这样可以便于处理错误 而且如果不想进行赋值 也可以这样 // await Goods.findOne({ _id: gid }).then(res => { // console.log(res) // ctx.status = 200 // ctx.body = res // }).catch(err => { // console.log(err) // ctx.status = 500 // ctx.body = { msg: 'not found', name: 'goods' } // }) // 因为商品详情中有评价 所以需要进行联表查询 // 使用 aggregate 聚合管道 有以下几个管道操作符 还有其他的 这里并没有列举完 // !$project 增加,删除,重命名字段 比如 order.aggregate([{ $project:{ order_id:1 }]) 那么会查询order的所有条目 // !但是输出值会输出条目的order_id字段(还有默认的_id) 还可以对该字段重命名 // !比如将order_id命名为oid 命名的操作为order.aggregate([{ $project:{ "oid":"$order_id" }]) // !$match 条件匹配,只有满足条件的文档才能进入下一阶段 // !比如 order.aggregate([{$project:{ order_id:1}, { $match:{ price:{ $lt:90 } } }]) // !就会在上面的基础上筛选出price字段小于90的结果 // *$limit 限制结果的数量 // *$skip 跳过文档的的数量 // *$sort 条件排序 // *$group 条件组合结果 分组也挺重要的 可以自己试一下 // !$lookup 可以引入其他集合的数据 也就是关联查询的重点! await Goods.aggregate([ // aggregate这里面匹配_id不能直接使用字符串 需要先转换为ObjectId mongoose.Types.ObjectId(gid) 这样转换 { $match: { '_id': mongoose.Types.ObjectId(gid) } }, { // 这里查询出来放在了 evaluations 数组里 但是我一直找不到怎样才能继续筛选这个数组 比如说 查询的结果很多 但是我只需要5条 还可以优化性能 // 但是一直找不到 但是可以换一种方法 那就是不去关联查询 直接分两次查询 一次查询goods 一次查询evaluations // 我这里数据不多就不更改了 $lookup: { // 关联查询 一个$lookup表示与一个表关联 如果想与多个表关联的话 可以添加多个$lookup from: 'evaluations', // 与那个表进行关联 localField: '_id', // 与Goods哪个字段进行关联 foreignField: 'gid', // 与evaluations表哪个字段进行关联 as: 'evaluations' // 查询出来的结果放到哪个字段中 // 上面是关联查询两个有联系的表 如果向关联查询两个没有联系的表 可以使用pipeline字段 传入一个数组 // 比如pipeline:[{$match:{year:2018}}] } }, ]).then(res => { console.log(res) ctx.status = 200 ctx.body = res }).catch(err => { console.log(err) ctx.status = 500 ctx.body = err }) }) ``` ## 13. mongoose批量指定删除 ```js router.delete('/deleteCart', async ctx => { // 在node中前端向后端发送数组的话 非常简单 像get,delete这种请求就直接接收就是了 比如发送的cid:[] 后端直接ctx.query['cid[]'] const cid = ctx.query['cid[]'] // 注意 这里使用deleteMany()与remove()是一样的 remove()是deleteMany的别名 await Cart.deleteMany({ // 重点是这里 这里的匹配规则 $in 匹配_id字段所有的cid数组里面的所有元素 _id: { $in: cid } }).then(res => { console.log(res, 'res') }).catch(err => { console.log(err, 'err') }) ctx.body = 123 }) ``` ## 14. mongoose对象数组关联查询 ```js // 这是orderSchema 的定义 const orderSchema = new mongoose.Schema({ uid: { type: mongoose.Types.ObjectId, required: true }, orderGoods: { type: [ // 这里是自定义的对象数组 // mongoose可以设置type为Object,可以直接像这样设置为{} 可以设置为Object 还可以设置为 mongoose.Types.Mixed 都是一样的1 { detail: { type: mongoose.Types.ObjectId, required: true, // 这里是使用populate的关键点 使用哪个集合来进行填充 这里就是goods表格 // 注意 要给detail字段传递对应的_id Number,String,ObjectId都可以作为refs使用 但是最好使用ObjectId 除非你很猛 ref: 'goods' }, goods_num: { type: Number, required: true } } ] }, date: { type: Date, default: Date.now() }, enTotal: { type: Number, required: true }, zhTotal: { type: Number, required: true } }) ``` ```js // 这是进行查询的操作 const uid = ctx.state.user.id // !!! 非常重要 这里是mongoose的对象数组 关联查询 就算不是对象数组 也可以使用populate 进行关联查询 // *注意 这里的 gid 是orderGoods的ref属性 具体的看model下的order.js populate还有其他选项可以看文档 await Order.find({ uid }).populate('orderGoods.gid').then(res => { if (res.length) { console.log('getOrder成功', res) ctx.status = 200 ctx.body = res } }).catch(e => { console.log('getOrder报错', e) ctx.status = 500 ctx.body = e }) ``` ## 15. mongoose使用含参数的方法注意! 注意mongoose里面的方法 会遵守这条语句 (如果传递回调,则执行查询,否则查询对象为returned.) 也就是如果需要传递参数的方法 最好加上await 或者then 不然会执行不成功