- 上一篇传送门—— 微信公众号 | 模块化工程文件
- 本次工程项目在 https://gitee.com/ayachensiyuan/wechat_server 代码仓库中更新
本篇的主题
- MongoDB的初识
- mongoDB compass的使用
- mongoose包的使用
- 数据模型设计
- ’每日经文‘案例的重写
以会话为基础的设计模式
在之前的设计中,服务器只能对用户的即时消息做出反应,不能进行连续的对话,根本原因是服务器没有储存功能,相当于是没有记忆功能的大脑。每当处理完用户的话语后数据就丢失了,所以数据库的引入是后期智能化聊天机器人的底层基础,同时服务器有了储存功能就能更好的为用户提供个性化的服务。
应该使用什么样的数据库?
数据库的分类有三种:
- 关系型数据库,如mysql
- 半关系型数据库,如mongodb
- 非关系型数据库,如文件ftp存储
非关系型数据库在代码层不容易操作基本上不用考虑,半关系型数据库相对于关系型数据库有些人会称为非关系型数据库也没有问题。
关系型数据库的各个表之间是可以使用sql语句实现CURD操作,所有的文档(对象)和字段(属性)分别以行和列表示。关系型数据库拥有严谨的存储形式,只能按照既定的规则进行操作,安全性大大提高,但存储这些既定的规则是要额外的存储空间的,所以所需用的空间也比半关系型数据库要大。
半关系型数据库如mongodb的存储方式像js中的对象,是以对象实体的形式存在于数据库中,对象实体中保存了该对象的所有属性和方法。对一个对象添加或删除一个属性或方法在数据库层面没有做任何限制,随意操作,通常会设计数据模型来控制CURD操作,如果没有这样的控制,后续维护会变得很困难也会引发数据安全问题。数据库仅仅保存了最基本的数据内容,所以占用的空间相比关系型的数据库要小得多。
在处理微信消息的事务中,其实这两种数据库都可以使用,因人而异。随着技术不断进步,在2022年的今天,操作非关系型数据库的成本大大降低,几乎可以做到和关系型数据库一样,不用太多纠结用哪种比较好,基本上没有区别。
我是使用js语言,操作非关系型数据库有很多的库直接使用,安全性和性能都大大提高,而且不需要花时间在设计阶段,直接专注业务就可以了。所以我使用mongodb来处理微信消息的事务。
特点 | 关系型数据库:mysql | 非(半)关系型数据库:mongodDB |
---|---|---|
数据库类型 | 行和列 | 类似json的文档 BSON |
操作方式 | 使用sql语句 | 使用类似对象的操作方法 |
查询速度 | 速度快 | 速度非常快 |
是否支持集合储存 | 否 | 是 |
扩展灵活性 | 低,只能按照既定的设计 | 高,可以随意对文档的属性扩展或删除 |
安全性 | 高 | 需要自行设计达到高安全性 |
适合储存数据 | 适合长期持久保存的数据 | 逻辑相对简单的海量数据 |
维护性 | 简单易于维护 | 维护性基于你的模型设计 |
成本 | 高,大多数据库都需要收费 | 低,基本都开源免费 |
mongoDB的初识
MongoDB文档: https://mongodb.net.cn/
-
由于我是直接在服务器中安装,登上服务器宝塔面板,在软件中搜索mongodb并点击安装。
-
使用mongo compass连接数据库
mongo compass是mangoDB官方推出的UI界面的操作数据库工具,关键是免费。当然你也可以使用其他的连接数据库的方式,如navicate等。接下来会根据mongo compass来做接下来的操作。-
下载mongo compass
下载网址: https://www.mongodb.com/try/download/compass
下载并本地安装 mongo compass
-
连接数据库
mongodb的官方数据库端口在27017,由于我们没有开放外部访问该端口的权限,所以连接方式就必须使用SSH通道连接,操作如下:
此处的用户名和密码是你服务器的用户名和密码。 -
创建新集合和数据库
点击左下方的 + 或点击如图所示创建一个新的数据库名字叫wechat_data,直接使用软件给到的提示操作即可。
在该数据库中同时创建一个users的集合
-
测试数据库
使用mongo compass的shell来测试连接数据库。自带的shell好处就是有智能提示,推荐使用。
use wechat_data
简单的测试代码如下,在users中添加两个文档:db.users.insertOne({name: ivanccc}) db.users.insertOne({first_name: 'foo', last_name: 'bar'}) db.users.find()
- 在mongo compass中查看添加的文档
点击刷新后看到添加成功的两个文档
- 注意事项
从上图中看到文档添加属性是没有什么限制的,上面的两个文档在同一个集合中但拥有不同的属性,这样随意的设计其实是很不利于后期的维护,甚至有安全隐患。所以为了提高数据库的性能,必须使用关系型数据库的设计思路,在同一个集合中必须保存同样类型的文档。借助mongoose库的模型设计,就能做的。
-
-
数据库操作常用命令
- show databases —— 显示所有数据库
- use < collectionName > —— 使用<集合>
- show collections —— 显示该数据库下的所有集合
- db.< collectionName >.insertOne() —— 在该集合中插入一条文档
- db.< collectionName >.deleteOne() —— 删除一条文档
- db.< collectionName >.updateOne(< filter >,{$set:< update >}) —— 更新文档的字段
- db.< collectionName >.find() —— 查询该集合下的所有文档
在服务器上使用数据库
借助mongoose包可以在node.js中控制数据库
- mongoose引入
使用vscode远程连接服务器工程项目。项目仓库在文章顶部链接,如有需求可以下载。在项目目录下打开终端安装mongoose。
npm install mongoose
- 新建mongodb入口文件 mongoose.js,并使用开发文档的示例代码测试连接。
mongoose文档地址:http://www.mongoosejs.net/docs/guide.html
//mongoose.js
const mongoose = require('mongoose')
//这里是内部访问,所以可以直接连接
mongoose.connect('mongodb://localhost/wechat_data')
//新建user模型
const User = mongoose.model('User', { appID: String })
//实例化对象
const user = new User({appID: '543221'})
//保存对象
user.save().then(()=>{
console.log('ok')
})
有人会发现以上操作并没有指定数据库的集合,为什么能在users集合中添加数据呢?
这是由于mongoose的底层机制设置,他会使用你模型的复数实体名字新建一个实体集,比如这里使用了user,对应就会在users中插入数据。如果你的数据库中没有它想要添加的集合名,就会自行创建一个集合。
使用mongoose插入一个文档就和面向对象编程创建一个对象一模一样,因为模型可以实现定义在其他文件中,操作数据库的基本模式就是:
- 使用模型来新建对象
- 对该对象进行操作
- 保存
非常的简单吧,但要想数据安全、高效就需要自己设计逻辑自洽的模型和高安全性的验证方式。
- 测试数据库连接
直接node mongoose.js运行后,并打开mongo compass查看以上运行有无正确添加数据。
可以看到该对象被正确创建,同时还生成"__v"字段,这个暂时可以无视。
模型设计
接下来就要对用户的会话模型进行设计,按照文本消息为例,每次的对话数据都会生成以下几个关键字段:
- 发送方appID
- 接收方appID
- 消息类型
- 时间戳
- 消息内容
- 消息ID
根据用发来的消息,我们能确定两个主体信息;一个是用户实体,另一个是内容实体;内容实体根据内容的不一样也可以按照需求分成多种内容会话。如我之前服务器的案例’发送每日经文‘就是一个姑且称之为每日经文会话。一个实体对应数据库的一个集合,目前我们就有两个集合。
1. 用户集合 users
2. 每日经文会话集合 versesessions
3. 此外获取的经文内容可以持久化保存在数据库中,这样避免过多的请求影响性能,所以再建一个每日经文的集合 dailyverses
我的思路:
在用户订阅时候创建用户的数据,每当用户发送内容给服务器,就在用户里新建一个新的会话,同时在该会话对应的会话类型的集合中同样生成该会话的详细内容。
这两个集合直接使用会话ID来连接在一起。
进入特定会话会有个触发的关键语句,在用户输入该关键语句时候新建特定的会话。
会话会包含判断会话是否结束的字段来判断这个会话有没有结束,服务器会对未结束的会话进行继续询问,以达到连续性的需求。
当服务器完成收集会话所有的数据后结束该会话。
以下代码会涉及到mongoose的api和mongoose schema的概念,需要自行查阅文档学习。
创建用户数据
- 用户模型
按照目前功能的需求,只需要以下两个字段:
a. 用户身份的标识是通过appid来识别的
b. 还要sessions的数组字段,保存了会话的id、时间和类型。 - 创建时机
我选用当用户订阅公众号时会创建用户的文档,一直会保存。 - 代码展示
用户数据模型
//userSchema,js
const Schema = require('mongoose').Schema
const userSchema = new Schema({
userID: {
type: String,
unique: true //用户创建的唯一性标识
},
sessions: [{
sessionID: String,
sessionType: String,
createDate: Number
}]
})
module.exports = userSchema
修改处理系统消息的中间件
// sysMsgMW.js
const sysMsgMW = require('express').Router()
sysMsgMW.route('/wx')
.post((req, res, next) => {
// event代表系统消息
if (req.jsonData.MsgType[0] == 'event') {
//subscribe代表订阅,在订阅时候
//新建用户数据库
if (req.jsonData.Event[0] == 'subscribe') {
const mongoose = require('mongoose')
const userSchema = require('../../model/userSchema')
const User = mongoose.model('user', userSchema)
// 2次验证,防止重复保存
User.findOne({ userID: req.jsonData.FromUserName[0] }).then(isExist => {
if (!isExist) {
const newUser = new User({
userID: req.jsonData.FromUserName[0]
})
newUser.save()
}
})
}
}
next()
})
module.exports = sysMsgMW
微信服务器在短时间内未收到个人服务器的响应时候会重新再发送同样的请求,这样情况最多会重复3次。因为某些api需求去调用外部的请求,过慢的网速也会使得微信服务器重新请求。我为了保证数据的唯一性,之后只要涉及到数据保存入库操作时,不仅在模型校验时设计了唯一性验证条件,同时在保存入库前还重新查询数据库进行重复性的二次校验,虽然影响了性能,但能大大提高数据可靠性。
添加会话
就每日经文会话而言
-
会话模型我设计了如下字段:
a. sessionID
b. userID
c. content
d. callback -
创建时机
当用户回复’每日经文‘时触发创建。 -
注意事项
a. 添加会话入用户sessions数组时,校验唯一性需要数组最后一个数据的重复性验证
b. 首先查询dailyverses集合中有无数据,如有就直接使用
c. 使用async await对异步数据镜像处理,避免逻辑复杂产生回调地狱 -
代码展示
versesession模型
//dailyVerseSessionSchema.js
const Schema = require('mongoose').Schema
const dailyVerseSessionSchema = new Schema({
sessionID: {
type: String,
unique: true //sessionID作为唯一性标识
},
userID: String,
content: String, // 用户发送的内容
callback: String // 服务器返回的内容
})
module.exports = dailyVerseSessionSchema
dailyverse模型
//dailyVerseSchema.js
const Schema = require('mongoose').Schema
const dailyVerseSchema = new Schema({
content: String,
verseDate: Number
})
module.exports = dailyVerseSchema
修改每日经文中间件代码
// dailyVerseMW.js
const dailyVerseMW = require('express').Router()
dailyVerseMW.route('/wx')
.post(async (req, res, next) => {
if (req.jsonData.MsgType[0] == 'text') {
const { jsonData } = req
//发送每日经文
if (jsonData.Content[0].trim() == '每日经文') {
//新建每日经文会话
const dailyVerseSession = {
sessionType: 'dailyVerse',
createDate: require('../../../tools/getDate')('today'),
sessionID: jsonData.MsgId[0]
}
const mongoose = require('mongoose')
const DailyVerse = mongoose.model('dailyVerse', require('../../../model/dailyVerseSchema'))
const User = mongoose.model('user', require('../../../model/userSchema'))
//在用户数据中添加会话
//重复交验
const repeatData = await User.findOne({userID: req.user.userID})
if(!repeatData || repeatData?.sessions[repeatData?.sessions.length -1]?.sessionID != jsonData.MsgId[0]){
await User.updateOne({ userID: req.user.userID }, { $push: { sessions: dailyVerseSession } })
}
//如果数据库没有今天经文就请求
//获得数据库数据
// 今天日期,这个函数自己封装的,会返回当天年月日拼接的数字
const today = require('../../../tools/getDate')('today')
const todayVerse = await DailyVerse.findOne({ verseDate: today })
if (!todayVerse) {
const axios = require('axios')
//使用axios来处理请求
axios.get(require('../../../config').dailyverseServer).then(response => {
//保存数据入库
const verse = {
content: response.data,
verseDate: today
}
const dailyVerse = new DailyVerse(verse)
//防止因网络延迟产生的重复数据
DailyVerse.findOne({verseDate: today}).then(isExist=>{
// console.log(isExist)
if(!isExist) dailyVerse.save()
})
//保存dailyverse session到数据库
const verseSession = {
sessionID: jsonData.MsgId[0],
userID: req.user.userID,
content: jsonData.Content[0],
callback: response.data
}
const VerseSession = mongoose.model('versesession', require('../../../model/dailyVerseSessionSchema'))
const newVerseSession = new VerseSession(verseSession)
//防止因网络延迟产生的重复数据
VerseSession.findOne({sessionID: jsonData.MsgId[0]}).then(isExist=>{
if(!isExist) newVerseSession.save()
})
//使用sendMsg方法发回收到的数据
const xmlSendData = require('../../../tools/sendMsg')(jsonData.FromUserName[0], jsonData.ToUserName[0], response.data)
res.end(xmlSendData)
})
} else {
//直接使用数据库数据
//保存dailyverse session到数据库
const verseSession = {
sessionID: jsonData.MsgId[0],
userID: req.user.userID,
content: jsonData.Content[0],
callback: todayVerse.content
}
const VerseSession = mongoose.model('versesession', require('../../../model/dailyVerseSessionSchema'))
const newVerseSession = new VerseSession(verseSession)
//防止因网络延迟产生的重复数据
VerseSession.findOne({sessionID: jsonData.MsgId[0]}).then(isExist=>{
if(!isExist) newVerseSession.save()
})
//使用sendMsg方法发回收到的数据
const xmlSendData = require('../../../tools/sendMsg')(jsonData.FromUserName[0], jsonData.ToUserName[0], todayVerse.content)
res.end(xmlSendData)
}
} else
next()
}
})
module.exports = dailyVerseMW
效果展示
当用户在公众号发送’每日经文‘时候,我们这里展示服务器上的数据存储情况
- users集合
- versesessions集合
- dailyverses集合
小结
以上的案例初步介绍了使用mongoose储存结构化数据的方法,但这个案例并没有过多涉及会话的持续性的问题,在下次的案例中演示使用数据库如何持久化聊天场景的实现。