微信公众号制作记账功能(二)

上次微信公众号制作记账功能(一)把nodejs的环境搭好了,并且可以简单地回复用户地信息了,那么接下来就要正式实现我们地功能了。

为了方便大家查看,也方便大家自己部署到公众号上,我把代码放到github上了,有兴趣可以去看一下。

一、功能分发

因为我的订阅号的权限十分有限,那么基本上就只能靠用户的输入来猜测命令了,打开/routes/wx.js,找到处理普通文字消息的代码,进行如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 判断消息类型
if (json.xml.MsgType === 'text') {
// 普通文字消息
moneyManager.doQuery(json.xml).then(msg => {
if (!msg) {
tempBody.xml.Content = '你好,我在努力进步中哦...'
} else {
tempBody.xml.Content = msg
}
resbody = jParser.parse(tempBody)
res.send(resbody)
})
}

1、链式调用模块

首先我的需求是不止有记账这一种功能,可以想见,那么多命令,如果都在wx.js里判断,那以后光找功能就要找好久。

我的思路是,将命令划分为几个模块,然后每个模块有自己专门的命令分发方法,每个模块处理完之后,wx.js就只负责将处理的结果返回给用户。

那么怎么判断这个命令属于哪个模块呢?我的想法是利用Promise的链式调用,如果上一个模块没有返回值,就执行下一个模块的命令分发,直至匹配到一个模块。类似下面这种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
function modelA() {
return new Promise(function(resolve, reject) {}
};
function modelB() {
return new Promise(function(resolve, reject) {}
};
modelA().then((res) => {
if (!res) {
return modelB()
} else {
return ''
}
})

目前只定义了一个模块,暂时看不出效果,不过后面我会定义一个默认模块,即若前面的模块都没有匹配到命令时,进入该模块返回一个默认的回复信息。

2、命令识别

人工智能离用户很近,但离开发者很远~

首先,记账模块有三个子功能:记账、查询账单、统计,而且记账时我想让用户直接输入,因为输入就已经够麻烦了,不能再增加步骤了。

那么在分发的入口就先去匹配固定的命令,如果没有匹配到,再去匹配记账这个随机性很大的命令。而在匹配记账的过程中,我用的是比较笨的方法,定义一个用户可能会输入的数组,然后再去匹配。

下面放上代码:

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
// /service/money.js
const keywords = [ '吃饭', '购物', '工资', '日用', '交通', '零食', '运动', '娱乐', '通讯', '服饰', '住房', '居家', '社交', '旅行', '烟酒', '数码', '医疗', '书籍', '礼物', '快递', '水果', '蔬菜', '亲友', '彩票', '捐赠', '维修', '办公', '宠物', '学习', '汽车', '美容', '兼职', '理财', '礼金', '早餐', '午餐', '晚餐', '早饭', '午饭', '晚饭']
const mt = [-1, -1, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 1, 1, -1, -1, -1, -1, -1, -1]

// 功能分发
const doQuery = (payload) => {
return new Promise((resolve, reject) => {
// 在此处比较数量是否一致,防止输入时出错
if (process.env.NODE_ENV === 'development') {
console.log('记账关键词与盈亏数量对比:', keywords.length, mt.length)
}
if (payload.Content === '账单') {
queryBill(payload).then(msg => {
resolve(msg)
})
} else if (payload.Content === '统计') {
tongji(payload).then(msg => {
resolve(msg)
})
} else {
// 记账
addBill(payload).then(msg => {
resolve(msg)
})
}
})
}



// 记账
const addBill = ({FromUserName, CreateTime, Content, MsgId}) => {
return new Promise((res, rej) => {
let index = -1
keywords.some((item, idx) => {
if (Content.indexOf(item) >= 0) {
index = idx
return true
}
})
if (index >= 0) {
const price = parseFloat(Content.replace(keywords[index], ''))
if (!price) {
res('错误提示:未输入金额或金额格式错误\n所属模块:[记账]')
} else {
const tempTxt = mt[index] > 0 ? '收入' : '支出'
retMsg = `记录成功!\n${keywords[index]} ${tempTxt} ${price}元`
res(retMsg)
}
} else {
res('')
}
})
}

// 查询账单
const queryBill = ({FromUserName, CreateTime}) => {
return new Promise((res, rej) => {
var str = '您的账单如下:\n'
res(str)
})
})
}

// 统计
const tongji = () => {

}

module.exports = {
doQuery
}

我将模块都放在service目录,以便统一管理。上面的代码Promise用得总感觉有点别扭,但还是可以正常运行的,等哪天开窍了估计就能写出更优雅的代码了。

二、数据存储

记账记账,如果不存下来,那还叫什么记账?

1、简单介绍

nodejs在数据存储方面的选择其实有很多种,但限于我的服务器很菜,我只能选择基于文件系统的数据库了,那就排除掉MySql、Redis。然后我听人推荐LevelDb,好家伙,感觉比Redis难用多了,我就想简单地放个对象数组,放了大半天都失败了,还读取不到!然后又把目光放到Sqlite3上面,又是一个好家伙,我还要在代码中定义数据结构,这还是我印象中地js吗?

就在我彷徨不所知时,一道闪电击中了我的脑袋,nedb!赶忙在github上搜索,竟然搜到了这个数据库,然后赶紧试试能不能用,好不好用。用过之后,我只有一个感觉,丝滑~

我就稍微介绍一下它吧(太丝滑了,必须得多说几句啊),nedb是mongoDb的一个子集,mongoDb相信大家会有所耳闻,而子集,就是将mongoDb的常用操作抽取出来了,所以用着是真的顺手。啥也不说了,先放链接:

https://github.com/louischatriot/nedb

这是我生成的数据库文件,没错,只有一个文件,文件内容肉眼可读!一个字,强啊。

2、应用

nedb中,一个表就是一个文件(个人肤浅见解),因此管理表的任务就交给我们了。

我的做法是专门用一个文件/service/db.js去管理数据库信息:

1
2
3
4
5
6
7
8
var Datastore = require('nedb')
// 生成nedb的实例并自动打开数据库
var moneyDb = new Datastore({ filename: 'moneyLog.db', autoload: true })

// 这里我考虑到项目中不止会用到一张表,就用这种方式导出多个nedb的操作对象
module.exports = {
moneyDb
}

然后在/service/money.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
// 构造账单记录
const bill = {
msgId: MsgId,
type: keywords[index],
yk: mt[index],
price: price,
openid: FromUserName,
recordTime: CreateTime
}
// 没错,就这么简单就插入了
db.moneyDb.insert(bill)

// ...

var str = '您的账单如下:\n'
// 查询也很简单,还可以排序,具体的可以看github上的文档
db.moneyDb.find({openid: FromUserName}).sort({recordTime: -1}).exec((err, docs) => {
docs.forEach(item => {
const d = dateUtil.getNowSimpleFormatDate(new Date(CreateTime * 1000))
const s = `[${d}] ${item.type} ${item.price}元\n`
str += s
})
res(str)
})

做到这一步之后,就可以运行查看效果了。

可以看到,这里我专门取消关注后再新增的记录,一样可以获取到以前的记录。

当然,这个功能还有许多地方要完善,比如支持查询啦,支持翻页啦,这些就放到下次再讲吧。

三、自适应环境

这个标题实在想不到既简短又直击要害的了…

通常情况下,我们的开发环境和生产环境的配置信息是有所不同的,那我们每次切换环境都要重新手动修改配置文件吗?

这当然不行。

以前这种区分开发环境和生产环境的功能也用过,不过都是别人写好的模板,这次总算自己实现了一下。

原理很简单,我们的系统都是可以设置环境变量的,而我们平常运行项目执行的yarn start或者npm start其实都是执行的package.json中配置的scripts: {"start": "node xxxx.js"}这种命令。那我们就可以在执行这个命令之前先将系统的环境变量设为开发环境或生产环境,然后程序里读取当前是开发环境还是生产环境,然后加载不同的配置文件就可以了。

首先,了解一下设置环境变量的方式:

1
2
3
4
# windows系统
set NODE_ENV=development
# Linux/Mac系统
export NODE_ENV=development

不同操作系统的命令不一样,这确实给我造成了不小的困扰,但也只能在不同的系统上再手动改一下了。

然后修改packages.json

1
2
3
4
5
// 开发环境是windows,因此先设置为windows的命令
"scripts": {
"dev": "set NODE_ENV=development&& nodemon ./bin/www",
"prod": "set NODE_ENV=production&& nodemon ./bin/www"
},

上面用到了&&用于在同一行内区分两条命令,同时要注意的是,&&前面不能用空格分开!,这导致我一直无法区分开发环境和生产环境。

这样修改之后,在js代码中,就可以通过以下代码进行判断:

1
if (process.env.NODE_ENV === 'development'){}

这样设置完成基本上就可以了,但因为我想把代码放到github上让大家参考,又不能把自己的密钥之类的放在上面,那么我就还需要想点褶子。

首先,项目中要将开发环境和生产环境的配置文件分开,同时要将私密信息单独创建一个文件,还要有一个文件专门放不会变的配置信息。因此,我的config文件夹就变成了这个样子:

其中*.self.js是我配置私密信息的地方,然后在dev.js中获取,最后在index.js中判断环境后获取对应的配置信息。然后,我再将*.self.js的文件设置为忽略上传就可以了。不过这样的话我又要去写README.md了。。。

下面附上index.js中区分环境的代码:

1
2
3
4
5
6
7
8
9
10
const devConfig = require('./dev')
const prodConfig = require('./prod')

// 区分开发环境和生产环境
const env = process.env.NODE_ENV || 'development'
var isDev = env === 'development'
// 每次启动时会打印当前运行环境,更好的提醒使用者
console.log('当前运行环境:',env)

const appid = isDev ? devConfig.appid : prodConfig.appid

总结

在做这个记账功能的时候,数据库的选择真的是卡了我好长时间,还好有那一道闪电~

最后,希望大家可以关注一下我的公众号,体验一下微信记账的感觉~

Godbobo wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
坚持原创技术分享,您的支持将鼓励我继续创作!