### 体验
~~想要玩一下的可以扫描以下二维码~~:
由于微信要取消云开发基础套餐的免费使用了,而本人暂无精力完善此项目,这个月(22.10)20号会清除本项目的云开发数据,目前已将已有数据备份,有机会会再放出来给大家体验的!
不过还是老样子,大家有什么需求或问题都可以提一下issue,我会竭力帮大家解决的~
### 自行部署
1. 由于本项目依托微信小程序提供的云开发能力,因此需要一些注册等的基本操作,可以参考我的另一个项目...的指引,如果会申请小程序使用云开发能力的可以朋友可以略过这一步:[GuGuMusic的使用方法](https://github.com/Mint-green/GuGumusic#%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95)
2. 下载基础数据库的文件,最近还是没能力完善说明各个表格的具体字段等,大家可以查看数据后大致判断,[度盘链接](https://pan.baidu.com/s/1LR6Q6BojBTQ0ywWiJVFX6w),提取码:dddd
3. cloudfunctions文件夹在的云函数右键部署,在云开发服务的地方也按照2中的文档建好并导入需要的数据后,应该就可以用了
### 更多
最近比较忙,先简单列列已完成的and放放效果图(请原谅我放那么多图),详细的介绍之后再上,持续更新ing~
有问题都可以提问,有什么想法也可以提一提呀~
### 更新日志
**22.10.02** 修复第一个用户(普通/微信)无法创建成功问题
**22.10.02** 由于微信调整云开发计费规则,本项目小程序测试版将于22年10月中旬停止开放
================================================
FILE: cloudfunctions/statisticRouter/config.json
================================================
{
"permissions": {
"openapi": [
]
}
}
================================================
FILE: cloudfunctions/statisticRouter/index.js
================================================
// 云函数入口文件
const cloud = require('wx-server-sdk')
const TcbRouter = require('tcb-router') // 导入小程序路由
const rescontent = require('utils/response_content.js')
cloud.init({ env: 'music-cloud-1v7x1' }) // 此处请切换为你自己的小程序云环境 id
const db = cloud.database({ throwOnNotFound: false })
const _ = db.command
const $ = db.command.aggregate
// 云函数入口函数
exports.main = async (event, context) => {
// const wxContext = cloud.getWXContext()
const app = new TcbRouter({ event })
console.log(event.$url)
app.use(async (ctx, next) => {
console.log('router name:', event.$url)
await next() // 执行下一中间件
});
app.router('getWBLearnData', async (ctx, next) => {
let user_id = event.user_id
let wd_bk_id = event.wd_bk_id
try {
// 某书的学习情况(区分未学习、学习中、已掌握)(原方案耗时较长,使用两个同步查询替换)
// let res = await db.collection('word_in_book')
// .aggregate()
// .match({ // 从词书与单词的关系表里获取当前学习的书的所有单词
// wd_bk_id: wd_bk_id
// })
// .lookup({ // lookup-1,从学习记录中匹配学过的单词
// from: 'learning_record',
// let: {
// wordId: '$word_id',
// },
// pipeline: $.pipeline()
// .match(_.expr($.and([
// $.eq(['$user_id', user_id]),
// $.eq(['$word_id', '$$wordId']),
// ])))
// .done(),
// as: 'word_list'
// })
// .replaceRoot({
// newRoot: $.mergeObjects([$.arrayElemAt(['$word_list', 0]), '$$ROOT'])
// })
// .group({
// _id: {
// list_size: $.size('$word_list'),
// is_master: '$master'
// },
// num: $.sum(1)
// })
// .end()
let learnedRes = db.collection('learning_record')
.aggregate()
.match({ // 从学习记录中筛选当前用户学过的所有单词
user_id: user_id,
})
.lookup({ // lookup-1,从词书词表中匹配在所学词书中的单词
from: 'word_in_book',
let: {
wordId: '$word_id',
},
pipeline: $.pipeline()
.match(_.expr($.and([
$.eq(['$wd_bk_id', wd_bk_id]),
$.eq(['$word_id', '$$wordId']),
])))
.done(),
as: 'word_list'
})
.match(_.expr( // 匹配已经学过的单词
$.eq([$.size('$word_list'), 1]),
))
.group({ // 根据是否掌握分类并计数
_id: '$master',
num: $.sum(1)
})
.end()
// {list:[{_id:true, num:xxx}, {_id:false, num:xxx}]}
let totalRes = db.collection('word_in_book')
.aggregate()
.match({
wd_bk_id: wd_bk_id
})
.count('total')
.end()
// {list:[{total:xxx}]}
let resList = await Promise.all([learnedRes, totalRes])
let bkLearnData = { notLearn: 0, learn: 0, master: 0 }
for (let i = 0; i < resList[0].list.length; i++) {
if (resList[0].list[i]['_id']) {
bkLearnData.master = resList[0].list[i].num
}
bkLearnData.learn += resList[0].list[i].num
}
let total = 0
if (resList[1].list.length > 0 && resList[1].list[0].total >= 0) total = resList[1].list[0].total
bkLearnData.notLearn = total - bkLearnData.learn
ctx.body = { ...rescontent.SUCCESS, data: bkLearnData }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('getAllWBData', async (ctx, next) => {
try {
let total = (await db.collection('word_book').count()).total
let batchTimes = Math.ceil(total / 10)
let tasks = []
for (let i = 0; i < batchTimes; i++) {
let promise = db.collection('word_book').skip(i * 10).limit(10).get()
tasks.push(promise)
}
let resList = await (await Promise.all(tasks)).reduce((acc, currentValue, i) => {
console.log('batch', i, 'done')
return {
data: acc.data.concat(currentValue.data),
errMsg: acc.errMsg,
}
}, { data: [] })
ctx.body = { ...rescontent.SUCCESS, data: resList.data }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('getSingleWBData', async (ctx, next) => {
let wd_bk_id = event.wd_bk_id
try {
let res = await db.collection('word_book').where({ wd_bk_id }).get()
let bkDetail = {
name: res.data[0].name,
description: res.data[0].description,
total: res.data[0].total,
coverType: res.data[0].cover_type
}
if (bkDetail.coverType == 'color') {
bkDetail.color = res.data[0].color
} else if (bkDetail.coverType == 'pic') {
bkDetail.coverUrl = res.data[0].cover_url
}
ctx.body = { ...rescontent.SUCCESS, data: bkDetail }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('getAllLearnData', async (ctx, next) => {
let user_id = event.user_id
try {
let res = await db.collection('learning_record')
.aggregate()
.match({ // 从词书与单词的关系表里获取当前学习的所有单词
user_id: user_id,
})
.group({
_id: '$master',
num: $.sum(1)
})
.end()
let allLearnData = { learn: 0, master: 0 }
for (let i = 0; i < res.list.length; i++) {
allLearnData.learn += res.list[i].num
if (res.list[i]['_id']) allLearnData.master = res.list[i].num
}
ctx.body = { ...rescontent.SUCCESS, data: allLearnData }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('getTodayLearnData', async (ctx, next) => {
let user_id = event.user_id
let now = new Date()
now.setMilliseconds(0)
now.setSeconds(0)
now.setMinutes(0)
now.setHours(0)
let date = now.getTime()
try {
let res = await db.collection('daily_sum')
.aggregate()
.match({ // 获取时间为当天的学习数据
user_id: user_id,
date,
})
.project({
_id: 0,
l_time: 1,
learn: 1,
review: 1,
})
.end()
let data = {
l_time: 0,
learn: 0,
review: 0,
}
if (res.list.length != 0) data = res.list[0]
ctx.body = { ...rescontent.SUCCESS, data }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('getDailySum', async (ctx, next) => {
let user_id = event.user_id
let skip = event.skip
if (skip == undefined) skip = 0
let now = new Date().getTime()
try {
let res = await db.collection('daily_sum')
.where({
user_id: user_id,
date: _.lte(now)
})
// .count()
.field({
_id: false,
date: true,
learn: true,
review: true,
})
.orderBy('date', 'desc')
.skip(skip)
.limit(10)
.get()
// 当判断到获取数量少于10(包括0)则表示已经取完了
ctx.body = { ...rescontent.SUCCESS, data: res.data }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('getNoteBookWord', async (ctx, next) => {
let user_id = event.user_id
let skip = event.skip
if (skip == undefined) skip = 0
let getNum = event.num
if (getNum == undefined) getNum = 20
let batchTimes = Math.ceil(getNum / 10)
try {
let tasks = []
for (let i = 0; i < batchTimes; i++) {
let num = i * 10 + 10 > getNum ? getNum - (i * 10) : 10
let skipNum = skip + i * 10
let promise = db.collection('notebook')
.aggregate()
.match({
user_id: user_id,
})
.project({
_id: 0,
word_id: 1,
})
.lookup({ // 从单词库中获取单词信息,默认从word找,没有再单独取word_all找
from: 'word',
let: {
wordId: '$word_id',
},
pipeline: $.pipeline()
.match(_.expr(
$.eq(['$word_id', '$$wordId']),
))
.project({
_id: 0,
word: 1,
translation: 1,
})
.done(),
as: 'word_detail'
})
.skip(skipNum)
.limit(num)
.end()
tasks.push(promise)
}
let resList = (await Promise.all(tasks)).reduce((acc, currentValue, index) => {
// console.log(acc)
acc = acc.concat(currentValue.list)
// console.log(currentValue)
return acc
}, [])
// console.log('resList', resList)
let notInSmallDB = []
let notInSmallDBIndex = []
let data = []
for (let i = 0; i < resList.length; i++) {
let translation = ''
let word = ''
if (resList[i].word_detail.length == 0) {
notInSmallDB.push(resList[i].word_id)
notInSmallDBIndex.push(i)
} else {
translation = resList[i].word_detail[0].translation
word = resList[i].word_detail[0].word
}
data.push({
word_id: resList[i].word_id,
word,
translation
})
}
// console.log('data', data)
// 接下来进行小数据库中找不到的词的数据获取
if (notInSmallDB.length > 0) {
let res = await db.collection('word_all')
.aggregate()
.match({
word_id: _.in(notInSmallDB),
})
.project({
_id: 0,
word_id: 1,
word: 1,
translation: 1,
})
.end()
for (let j = 0; j < res.list.length; j++) {
let i = notInSmallDB.indexOf(res.list[j].word_id)
let index = notInSmallDBIndex[i]
data[index] = {
word_id: res.list[j].word_id,
word: res.list[j].word,
translation: res.list[j].translation
}
}
}
ctx.body = { ...rescontent.SUCCESS, data: data }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('getBkLearnedWord', async (ctx, next) => {
let user_id = event.user_id
let wd_bk_id = event.wd_bk_id
let skip = event.skip
if (!skip) skip = 0
try {
let res = await db.collection('learning_record')
.aggregate()
.match({
user_id: user_id,
})
.lookup({ // lookup-1,筛选在所学词书中的单词
from: 'word_in_book',
let: {
wordId: '$word_id',
},
pipeline: $.pipeline()
.match(_.expr($.and([
$.eq(['$wd_bk_id', wd_bk_id]),
$.eq(['$word_id', '$$wordId']),
])))
.done(),
as: 'word_list'
})
.match(_.expr(
$.eq([$.size('$word_list'), 1]),
))
.skip(skip)
.limit(20)
.lookup({ // lookup-2,获取已取得单词的详细信息
from: 'word',
let: {
wordId: '$word_id',
},
pipeline: $.pipeline()
.match(_.expr(
$.eq(['$word_id', '$$wordId']),
))
.project({
_id: 0,
word: 1,
translation: 1,
})
.done(),
as: 'word_detail'
})
.replaceRoot({
newRoot: $.mergeObjects([$.arrayElemAt(['$word_detail', 0]), '$$ROOT'])
})
.project({
_id: 0,
word: 1,
word_id: 1,
translation: 1,
})
.end()
ctx.body = { ...rescontent.SUCCESS, data: res.list }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('getBkMasteredWord', async (ctx, next) => {
let user_id = event.user_id
let wd_bk_id = event.wd_bk_id
let skip = event.skip
if (!skip) skip = 0
try {
let res = await db.collection('learning_record')
.aggregate()
.match({
user_id: user_id,
master: true,
})
.lookup({ // lookup-1,筛选在某本书里的单词
from: 'word_in_book',
let: {
wordId: '$word_id',
},
pipeline: $.pipeline()
.match(_.expr($.and([
$.eq(['$wd_bk_id', wd_bk_id]),
$.eq(['$word_id', '$$wordId']),
])))
.done(),
as: 'word_list'
})
.match(_.expr(
$.eq([$.size('$word_list'), 1]),
))
.skip(skip)
.limit(20)
.lookup({ // lookup-2,获取已取得单词的详细信息
from: 'word',
let: {
wordId: '$word_id',
},
pipeline: $.pipeline()
.match(_.expr(
$.eq(['$word_id', '$$wordId']),
))
.project({
_id: 0,
word: 1,
translation: 1,
})
.done(),
as: 'word_detail'
})
.replaceRoot({
newRoot: $.mergeObjects([$.arrayElemAt(['$word_detail', 0]), '$$ROOT'])
})
.project({
_id: 0,
word: 1,
word_id: 1,
translation: 1,
})
.end()
ctx.body = { ...rescontent.SUCCESS, data: res.list }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('getBkUnlearnedWord', async (ctx, next) => {
let user_id = event.user_id
let wd_bk_id = event.wd_bk_id
// console.log('user_id', user_id)
// console.log('wd_bk_id', wd_bk_id)
let skip = event.skip
if (!skip) skip = 0
try {
let res = await db.collection('word_in_book')
.aggregate()
.match({
wd_bk_id: wd_bk_id,
})
.sort({
wd_index: 1,
})
.lookup({ // lookup-1,筛选在未学过的单词
from: 'learning_record',
let: {
wordId: '$word_id',
},
pipeline: $.pipeline()
.match(_.expr($.and([
$.eq(['$user_id', user_id]),
$.eq(['$word_id', '$$wordId']),
])))
.done(),
as: 'word_list'
})
.match(_.expr(
$.eq([$.size('$word_list'), 0]),
))
.skip(skip)
.limit(20)
.lookup({ // lookup-2,获取已取得单词的详细信息
from: 'word',
let: {
wordId: '$word_id',
},
pipeline: $.pipeline()
.match(_.expr(
$.eq(['$word_id', '$$wordId']),
))
.project({
_id: 0,
word: 1,
translation: 1,
})
.done(),
as: 'word_detail'
})
.replaceRoot({
newRoot: $.mergeObjects([$.arrayElemAt(['$word_detail', 0]), '$$ROOT'])
})
.project({
_id: 0,
word: 1,
word_id: 1,
translation: 1,
})
.end()
ctx.body = { ...rescontent.SUCCESS, data: res.list }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('getBkWord', async (ctx, next) => {
let wd_bk_id = event.wd_bk_id
let skip = event.skip
if (!skip) skip = 0
try {
let res = await db.collection('word_in_book')
.aggregate()
.match({
wd_bk_id: wd_bk_id,
})
.sort({
wd_index: 1,
})
.skip(skip)
.limit(20)
.lookup({ // lookup-2,获取已取得单词的详细信息
from: 'word',
let: {
wordId: '$word_id',
},
pipeline: $.pipeline()
.match(_.expr(
$.eq(['$word_id', '$$wordId']),
))
.project({
_id: 0,
word: 1,
translation: 1,
})
.done(),
as: 'word_detail'
})
.replaceRoot({
newRoot: $.mergeObjects([$.arrayElemAt(['$word_detail', 0]), '$$ROOT'])
})
.project({
_id: 0,
word: 1,
word_id: 1,
translation: 1,
})
.end()
ctx.body = { ...rescontent.SUCCESS, data: res.list }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('getLearnedWord', async (ctx, next) => {
let user_id = event.user_id
let skip = event.skip
if (!skip) skip = 0
try {
let res = await db.collection('learning_record')
.aggregate()
.match({
user_id: user_id,
})
.skip(skip)
.limit(20)
.lookup({ // 获取已取得单词的详细信息
from: 'word',
let: {
wordId: '$word_id',
},
pipeline: $.pipeline()
.match(_.expr(
$.eq(['$word_id', '$$wordId']),
))
.project({
_id: 0,
word: 1,
translation: 1,
})
.done(),
as: 'word_detail'
})
.replaceRoot({
newRoot: $.mergeObjects([$.arrayElemAt(['$word_detail', 0]), '$$ROOT'])
})
.project({
_id: 0,
word: 1,
word_id: 1,
translation: 1,
})
.end()
ctx.body = { ...rescontent.SUCCESS, data: res.list }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('getMasteredWord', async (ctx, next) => {
let user_id = event.user_id
let skip = event.skip
if (!skip) skip = 0
try {
let res = await db.collection('learning_record')
.aggregate()
.match({
user_id: user_id,
master: true,
})
.skip(skip)
.limit(20)
.lookup({ // 获取已取得单词的详细信息
from: 'word',
let: {
wordId: '$word_id',
},
pipeline: $.pipeline()
.match(_.expr(
$.eq(['$word_id', '$$wordId']),
))
.project({
_id: 0,
word: 1,
translation: 1,
})
.done(),
as: 'word_detail'
})
.replaceRoot({
newRoot: $.mergeObjects([$.arrayElemAt(['$word_detail', 0]), '$$ROOT'])
})
.project({
_id: 0,
word: 1,
word_id: 1,
translation: 1,
})
.end()
ctx.body = { ...rescontent.SUCCESS, data: res.list }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('getReviewWord', async (ctx, next) => {
const user_id = event.user_id
let skip = event.skip
if (!skip) skip = 0
try {
let res = await db.collection('learning_record')
.aggregate()
.match({ // 选取还未掌握的单词
user_id: user_id,
master: false,
})
.skip(skip)
.limit(20)
.lookup({ // 获取取得的单词的详细数据
from: 'word',
localField: 'word_id',
foreignField: 'word_id',
as: 'word_detail'
})
.replaceRoot({ // 把单词详情合并到对象属性中
newRoot: $.mergeObjects([$.arrayElemAt(['$word_detail', 0]), '$$ROOT'])
})
.project({
_id: 0,
word_id: 1,
word: 1,
translation: 1,
})
.end()
ctx.body = { ...rescontent.SUCCESS, data: res.list }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('getTodayLearnWord', async (ctx, next) => {
const user_id = event.user_id
let skip = event.skip
if (!skip) skip = 0
let now = new Date()
now.setMilliseconds(0)
now.setSeconds(0)
now.setMinutes(0)
now.setHours(0)
let date = now.getTime()
try {
let res = await db.collection('learning_record')
.aggregate()
.match({ // 选取还未掌握的单词
user_id: user_id,
c_time: date,
})
.skip(skip)
.limit(20)
.lookup({ // 获取取得的单词的详细数据
from: 'word',
localField: 'word_id',
foreignField: 'word_id',
as: 'word_detail'
})
.replaceRoot({ // 把单词详情合并到对象属性中
newRoot: $.mergeObjects([$.arrayElemAt(['$word_detail', 0]), '$$ROOT'])
})
.project({
_id: 0,
word_id: 1,
word: 1,
translation: 1,
})
.end()
ctx.body = { ...rescontent.SUCCESS, data: res.list }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('getTodayReviewWord', async (ctx, next) => {
const user_id = event.user_id
let skip = event.skip
if (!skip) skip = 0
let now = new Date()
now.setMilliseconds(0)
now.setSeconds(0)
now.setMinutes(0)
now.setHours(0)
let date = now.getTime()
try {
let res = await db.collection('learning_record')
.aggregate()
.match({ // 选取还未掌握的单词
user_id: user_id,
last_l: date,
c_time: _.neq(date),
})
.skip(skip)
.limit(20)
.lookup({ // 获取取得的单词的详细数据
from: 'word',
localField: 'word_id',
foreignField: 'word_id',
as: 'word_detail'
})
.replaceRoot({ // 把单词详情合并到对象属性中
newRoot: $.mergeObjects([$.arrayElemAt(['$word_detail', 0]), '$$ROOT'])
})
.project({
_id: 0,
word_id: 1,
word: 1,
translation: 1,
})
.end()
ctx.body = { ...rescontent.SUCCESS, data: res.list }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
return app.serve()
}
================================================
FILE: cloudfunctions/statisticRouter/package.json
================================================
{
"name": "statisticRouter",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"wx-server-sdk": "~2.5.3",
"tcb-router": "^1.1.2"
}
}
================================================
FILE: cloudfunctions/statisticRouter/utils/response_content.js
================================================
const SUCCESS = { errorcode: 100, errormsg: "success" } //成功
const LOGINOK = { errorcode: 1, errormsg: "Login successfully" } //登录成功
const REGISTEROK= { errorcode: 2, errormsg: "Register successfully" } //注册成功
const DBERR = { errorcode: -1, errormsg: "Database error!" } //数据库操作失败
const ROUTERERR = { errorcode: -2, errormsg: "Wrong router name" } //路由名字有误
const LOGINERR = { errorcode: -3, errormsg: "Wrong username or pwd" } //登录信息有误
const DATAERR = { errorcode: -4, errormsg: "Wrong data!" } //数据有误
const UNKOWNERR = { errorcode: -100, errormsg: "Unkown error!" } //出现未知错误
module.exports={
SUCCESS: SUCCESS,
LOGINOK: LOGINOK,
REGISTEROK: REGISTEROK,
DBERR: DBERR,
ROUTERERR: ROUTERERR,
LOGINERR: LOGINERR,
DATAERR: DATAERR,
UNKOWNERR: UNKOWNERR,
}
================================================
FILE: cloudfunctions/userRouter/config.json
================================================
{
"permissions": {
"openapi": [
]
}
}
================================================
FILE: cloudfunctions/userRouter/index.js
================================================
// 云函数入口文件
const cloud = require('wx-server-sdk')
const TcbRouter = require('tcb-router') // 导入小程序路由
const rescontent = require('utils/response_content.js')
const InitOFMatrix = require('utils/init_of_matrix.js')
const DefaultAvatarList = require('utils/default_avatar_pic.js')
cloud.init({ env: 'music-cloud-1v7x1' }) // 此处请切换为你自己的小程序云环境 id
const db = cloud.database({ throwOnNotFound: false })
const learnerDB = db.collection('learner')
// 云函数入口函数
exports.main = async (event, context) => {
const app = new TcbRouter({ event })
// console.log('event:', event)
// console.log('context:', context)
// app.use 表示该中间件会适用于所有的路由
app.use(async (ctx, next) => {
console.log('router name:', event.$url)
await next() // 执行下一中间件
});
app.router('checkUsername', async (ctx, next) => {
let username = event.username
try {
let res = await learnerDB.where({
username
}).get()
let isFind = false
if (res.data.length > 0) {
isFind = true
}
ctx.body = { ...rescontent.SUCCESS, data: { isFind } }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('register', async (ctx, next) => {
let userinfo = {} // 构建新用户记录对象
let time = new Date()
console.log('Start handling request', time.getTime())
userinfo.username = event.username
userinfo.pwd = event.pwd
userinfo.c_time = time.toISOString()
userinfo.last_login = userinfo.c_time
userinfo.l_book_id = -1
userinfo.settings = {}
userinfo.open_id = ''
userinfo.wx_user = false
random_num = Math.floor(Math.random() * DefaultAvatarList.length)
userinfo.avatar_pic = DefaultAvatarList[random_num] || DefaultAvatarList[0]
userinfo.of_matrix = InitOFMatrix
try {
let res1 = await learnerDB.orderBy('user_id', 'desc').limit(1).get() // 获得当前最大的user_id
if (res1.data.length == 0) {
console.log('there\'s no other user, this is the first user his/her id will be 0')
userinfo.user_id = 0
} else {
console.log('Get last user_id, which is', res1.data[0].user_id, 'then creating account', new Date().getTime())
userinfo.user_id = res1.data[0].user_id + 1
}
let res2 = await learnerDB.add({ data: userinfo }) // 向数据库添加新用户记录
if (!res2._id) {
ctx.body = { ...rescontent.DBERR }
return
}
console.log('Create successfully, done.', new Date().getTime())
let returnInfo = {
username: userinfo.username,
last_login: userinfo.last_login,
l_book_id: userinfo.l_book_id,
settings: userinfo.settings,
wx_user: userinfo.wx_user,
avatar_pic: userinfo.avatar_pic,
user_id: userinfo.user_id,
}
ctx.body = { ...rescontent.REGISTEROK, data: returnInfo }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('login', async (ctx, next) => {
let time = new Date()
console.log('Start handling request', time.getTime())
let last_login = time.toISOString()
try {
let res1 = await learnerDB.where({
username: event.username,
pwd: event.pwd,
}).limit(1).field({ // 获取用户的基本数据(user_id、词书、设置等)
_id: false,
c_time: false,
open_id: false,
pwd: false,
of_matrix: false,
}).get()
if (res1.data.toString() == "") {
ctx.body = { ...rescontent.LOGINERR }
return
}
console.log('Get userinfo, then update login time', new Date().getTime())
// console.log(res1)
let res2 = await learnerDB.where({
username: event.username,
pwd: event.pwd,
}).update({
data: { last_login: last_login }
})
// console.log(res2)
if (res2.stats.updated == 0) {
ctx.body = { ...rescontent.DBERR }
return
}
console.log('Done', new Date().getTime())
ctx.body = { ...rescontent.LOGINOK, data: res1.data[0] }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('wxLogin', async (ctx, next) => {
let time = new Date()
console.log('Start handling request', time.getTime())
const wxContext = cloud.getWXContext()
let open_id = wxContext.OPENID
let username = event.username
try {
let res1 = await learnerDB.where({
open_id
}).limit(1).field({ // 尝试获取用户的基本数据(user_id、词书、设置等)
_id: false,
c_time: false,
open_id: false,
pwd: false,
}).get()
if (res1.data.toString() == "") { // 结果为空表示该用户没注册,需要创建相应记录
console.log('User not find, now create an account')
let userinfo = {}
userinfo.username = username
userinfo.pwd = ''
userinfo.c_time = time.toISOString()
userinfo.last_login = userinfo.c_time
userinfo.l_book_id = -1
userinfo.settings = { auto_update_avatar: true, auto_update_username: true }
userinfo.open_id = open_id
userinfo.wx_user = true
userinfo.avatar_pic = event.avatar_pic
userinfo.of_matrix = InitOFMatrix
let res2 = await learnerDB.orderBy('user_id', 'desc').limit(1).get()
if (res2.data.length == 0) {
console.log('there\'s no other user, this is the first user his/her id will be 0')
userinfo.user_id = 0
} else {
console.log('Get last user_id, which is', res2.data[0].user_id, 'then creating account', new Date().getTime())
userinfo.user_id = res2.data[0].user_id + 1
}
let res3 = await learnerDB.add({ data: userinfo })
if (!res3._id) {
ctx.body = { ...rescontent.DBERR }
return
}
let returnInfo = {
username: userinfo.username,
last_login: userinfo.last_login,
l_book_id: userinfo.l_book_id,
settings: userinfo.settings,
wx_user: userinfo.wx_user,
avatar_pic: userinfo.avatar_pic,
user_id: userinfo.user_id,
}
console.log('Create successfully, done.', new Date().getTime())
ctx.body = { ...rescontent.REGISTEROK, data: returnInfo }
return
} else { // 结果不为空表示改用户已注册,则更新上次登录时间
console.log('Find user, now update last login time')
let data = { last_login: time.toISOString() }
if (res1.data[0].settings.auto_update_avatar) {
data.avatar_pic = event.avatar_pic
res1.data[0].avatar_pic = event.avatar_pic
}
if (res1.data[0].settings.auto_update_username) {
data.username = event.username
res1.data[0].username = event.username
}
let res2 = await learnerDB.where({
open_id: open_id
}).update({
data
})
if (res2.stats.updated == 0) {
ctx.body = { ...rescontent.DBERR }
return
}
console.log('Done', new Date().getTime())
ctx.body = { ...rescontent.LOGINOK, data: res1.data[0] }
}
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('changeWordBook', async (ctx, next) => {
let user_id = event.user_id
let wd_bk_id = event.wd_bk_id
try {
let res = await db.collection('learner')
.where({
user_id
})
.update({
data: {
l_book_id: wd_bk_id,
}
})
console.log(res)
let data = false
if (res.stats.updated == 1) {
data = true
}
ctx.body = { ...rescontent.SUCCESS, data: data }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('changeSettings', async (ctx, next) => {
let user_id = event.user_id
let settings = event.settings
try {
let res = await db.collection('learner')
.where({
user_id
})
.update({
data: {
settings: settings,
}
})
console.log(res)
let data = false
if (res.stats.updated == 1) {
data = true
}
ctx.body = { ...rescontent.SUCCESS, data: data }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('changeUserInfo', async (ctx, next) => {
let user_id = event.user_id
let fieldName = event.type
let value = event.value
let validRange = ['username', 'avatar_pic', 'l_book_id', 'settings']
try {
let updateData = {}
if (typeof (fieldName) == 'string') {
if (validRange.indexOf(fieldName) == -1) {
ctx.body = { ...rescontent.DATAERR }
return
}
updateData[fieldName] = value
} else if (typeof (fieldName) == 'object' && typeof (fieldName[0]) == 'string') {
for (let i = 0; i < fieldName.length; i++) {
if (validRange.indexOf(fieldName[i]) == -1) {
ctx.body = { ...rescontent.DATAERR }
return
}
updateData[fieldName[i]] = value[i]
}
} else {
ctx.body = { ...rescontent.DATAERR }
return
}
let res = await db.collection('learner')
.where({
user_id
})
.update({
data: updateData
})
console.log(res)
let data = false
if (res.stats.updated == 1) {
data = true
}
ctx.body = { ...rescontent.SUCCESS, data: data }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('changePwd', async (ctx, next) => {
let user_id = event.user_id
let oldPwd = event.oldPwd
let newPwd = event.newPwd
try {
let res = await db.collection('learner')
.where({
user_id,
pwd: oldPwd,
})
.update({
data: {
pwd: newPwd
}
})
console.log(res)
let data = false
if (res.stats.updated == 1) {
data = true
}
ctx.body = { ...rescontent.SUCCESS, data: data }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('getUserInfoViaId', async (ctx, next) => {
let user_id = event.user_id
let time = new Date()
let last_login = time.toISOString()
try {
let updateRes = db.collection('learner')
.where({
user_id,
}).update({
data: { last_login: last_login }
})
let getRes = db.collection('learner')
.where({
user_id
})
.field({
_id: -1,
user_id: 1,
wx_user: 1,
username: 1,
avatar_pic: 1,
l_book_id: 1,
settings: 1,
last_login: 1,
})
.get()
let resList = await Promise.all([updateRes, getRes])
let state = false
if (resList[0].stats.updated == 1 && resList[1].data.length == 1) {
state = true
resList[1].data[0].last_login = last_login
}
if (state) {
ctx.body = { ...rescontent.SUCCESS, data: resList[1].data[0] }
} else {
ctx.body = { ...rescontent.LOGINERR, data: '自动登录失败' }
}
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
return app.serve()
}
================================================
FILE: cloudfunctions/userRouter/package.json
================================================
{
"name": "userRouter",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"tcb-router": "^1.1.2",
"wx-server-sdk": "~2.5.3"
}
}
================================================
FILE: cloudfunctions/userRouter/utils/default_avatar_pic.js
================================================
module.exports = [
'https://pic2.zhimg.com/50/v2-34395fd10798f4b5bad583d61f98c849_hd.jpg?source=1940ef5c',
'https://pic2.zhimg.com/50/v2-b1e4eb7f72908a04306958f13ce45d94_hd.jpg?source=1940ef5c',
'https://inews.gtimg.com/newsapp_bt/0/13804696252/1000',
'https://inews.gtimg.com/newsapp_bt/0/13808742009/1000'
]
================================================
FILE: cloudfunctions/userRouter/utils/init_of_matrix.js
================================================
module.exports = {
'1.3': [5],
'1.4': [5],
'1.5': [5],
'1.6': [5],
'1.7': [5],
'1.8': [5],
'1.9': [5],
'2.0': [5],
'2.1': [5],
'2.2': [5],
'2.3': [5],
'2.4': [5],
'2.5': [5],
'2.6': [5],
'2.7': [5],
'2.8': [5],
}
================================================
FILE: cloudfunctions/userRouter/utils/response_content.js
================================================
const SUCCESS = { errorcode: 100, errormsg: "success" } //成功
const LOGINOK = { errorcode: 1, errormsg: "Login successfully" } //登录成功
const REGISTEROK= { errorcode: 2, errormsg: "Register successfully" } //注册成功
const DBERR = { errorcode: -1, errormsg: "Database error!" } //数据库操作失败
const ROUTERERR = { errorcode: -2, errormsg: "Wrong router name" } //路由名字有误
const LOGINERR = { errorcode: -3, errormsg: "Wrong username or pwd" } //登录信息有误
const DATAERR = { errorcode: -4, errormsg: "Wrong data!" } //数据有误
const UNKOWNERR = { errorcode: -100, errormsg: "Unkown error!" } //出现未知错误
module.exports={
SUCCESS: SUCCESS,
LOGINOK: LOGINOK,
REGISTEROK: REGISTEROK,
DBERR: DBERR,
ROUTERERR: ROUTERERR,
LOGINERR: LOGINERR,
DATAERR: DATAERR,
UNKOWNERR: UNKOWNERR,
}
================================================
FILE: cloudfunctions/wordRouter/config.json
================================================
{
"permissions": {
"openapi": [
]
}
}
================================================
FILE: cloudfunctions/wordRouter/index.js
================================================
// 云函数入口文件
const cloud = require('wx-server-sdk')
const TcbRouter = require('tcb-router') // 导入小程序路由
// const request = require('request')
const format_time = require('utils/format_time.js')
const rescontent = require('utils/response_content.js')
const sm_5_js = require('utils/sm-5.js')
const get_all_sort_list = require('utils/get_all_sort_list.js')
const bent = require('bent')
cloud.init({ env: 'music-cloud-1v7x1' }) // 此处请切换为你自己的小程序云环境 id
const db = cloud.database({ throwOnNotFound: false })
const _ = db.command
const $ = db.command.aggregate
cloud.init()
// 云函数入口函数
exports.main = async (event, context) => {
// const wxContext = cloud.getWXContext()
const app = new TcbRouter({ event })
console.log(event.$url)
app.use(async (ctx, next) => {
console.log('router name:', event.$url)
await next() // 执行下一中间件
});
app.router('getDailySentence', async (ctx, next) => {
let time = new Date().getTime()
console.log(time)
let dateStr = format_time.formatDate(time)
console.log(dateStr)
// let requestUrl = [requestUrl_youdao, requestUrl_iciba, requestUrl_shanbay]
// console.log(requestUrl)
try {
let dailySentenceDB = db.collection('dailySentence')
let res = await dailySentenceDB.where({
date: dateStr
}).get()
if (res.data.toString() != "") {
ctx.body = { ...rescontent.SUCCESS, data: res.data[0].dailySentence }
return
}
console.log("Can't find", new Date().getTime())
const getJSON = bent('json')
let requestUrl_youdao = 'https://dict.youdao.com/infoline?mode=publish&date=' + dateStr + '&update=auto&apiversion=5.0'
let requestUrl_iciba = 'https://sentence.iciba.com/index.php?c=dailysentence&m=getdetail&title=' + dateStr
let requestUrl_shanbay = 'https://apiv3.shanbay.com/weapps/dailyquote/quote/?date=' + dateStr
let dailySentence = []
let promise1 = getJSON(requestUrl_youdao)
let promise2 = getJSON(requestUrl_iciba)
let promise3 = getJSON(requestUrl_shanbay)
let tasks = [promise1, promise2, promise3]
let resList = await Promise.all(tasks)
// for Youdao--------------------------------------------------------
let res1 = resList[0]
let result_list = res1[dateStr]
let dateNum = format_time.dateNum(time) * 10000
let i = 0
for (i; i < result_list.length; i++) {
if (result_list[i].startTime - dateNum < 10000 && result_list[i].voice && result_list[i].voice != '') { break }
}
// console.log('Youdao sentence', result_list[i])
dailySentence.push({
source: 'Youdao',
content: result_list[i].title,
translation: result_list[i].summary,
voiceUrl: result_list[i].voice
})
// ------------------------------------------------------------------
// for iCIBA---------------------------------------------------------
let res2 = resList[1]
dailySentence.push({
source: 'iCIBA',
content: res2.content,
translation: res2.note,
voiceUrl: res2.tts
})
// ------------------------------------------------------------------
// for Shanbay-------------------------------------------------------
let res3 = resList[2]
dailySentence.push({
source: 'Shanbay',
content: res3.content,
translation: res3.translation,
author: res3.author
})
// ------------------------------------------------------------------
console.log("request done", new Date().getTime())
console.log(dailySentence)
let t1 = new Date().toISOString()
let res4 = await dailySentenceDB.add({
data: {
date: dateStr,
c_time: t1,
dailySentence
}
})
// if (!res4._id) { // 获取即可,添加失败可让下一位有缘人请求的时候顺便添加
// ctx.body = { ...rescontent.DBERR }
// return
// }
console.log('Recording successfully, done.', new Date().getTime())
ctx.body = { ...rescontent.SUCCESS, data: dailySentence }
} catch (e) {
console.log(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('getSearchResult', async (ctx, next) => {
let keyword = event.keyword
let DBtype = event.DBtype
let DBname = (DBtype == 0) ? 'word' : 'word_all'
let getLemma = event.getLemma
let skip = event.skip
if (getLemma === undefined) getLemma = true
if (skip === undefined) skip = 0
let recordLimit = (DBtype == 0) ? 20 : 30
let zhExp = /[\u4e00-\u9fa5]/
let isTranslation = zhExp.test(keyword)
keyword = keyword.replace(/-/g, '')
keyword = keyword.replace(/\'/g, '\\\'')
keyword = keyword.replace(/\./g, '\\\.')
try {
console.log('keyword:', keyword)
if (!isTranslation && keyword.indexOf(' ') == -1) {
console.log('don\'t have space')
// 无空格情况
// 查找原型
let lemmaSearch = []
if (getLemma) {
let lemmaRes = await db.collection('lemma')
.where({
words: _.elemMatch(_.eq(keyword))
})
.field({
_id: false,
words: false,
})
.get()
console.log('lemmaRes:', lemmaRes)
if (lemmaRes.data.length > 0) {
// 若存在原型则获取其释义,首先从将结果转化成原型数组再对数组中的词获取详情
let lemmaWords = []
for (let i = 0; i < lemmaRes.data.length; i++) {
lemmaWords.push(lemmaRes.data[i].stem)
}
let stemDetailRes = await db.collection('word')
.where({
word: _.in(lemmaWords)
})
.field({
_id: false,
word: true,
word_id: true,
exchange: true,
translation: true,
})
.get()
if (stemDetailRes.data.length != lemmaWords.length) {
stemDetailRes = await db.collection('word_all')
.where({
word: _.in(lemmaWords)
})
.field({
_id: false,
word: true,
word_id: true,
exchange: true,
translation: true,
})
.get()
}
lemmaSearch = stemDetailRes.data
}
console.log('lemmaSearch:', lemmaSearch)
}
// 使用sw字段进行前缀模糊查找
let exp = new RegExp('^' + keyword + '.*', 'i')
// console.log('exp:', exp)
let prefixRes = await db.collection(DBname)
.where({
strip_word: exp,
})
.skip(skip)
.limit(recordLimit)
.field({
_id: false,
word: true,
word_id: true,
translation: true,
})
.get()
console.log('prefixRes.data', prefixRes.data)
ctx.body = {
...rescontent.SUCCESS,
data: {
lemmaSearch,
directSearch: prefixRes.data
}
}
} else if (!isTranslation) {
// 有空格情况,不进行原型查找,将空格换为任意位数通配符进行匹配
// 获取由空格分割的每个部分的索引并求和
let kwSpiltBySpace = keyword.split(' ')
keyword = keyword.replace(/ /g, '.*')
let exp = new RegExp('^.*' + keyword, 'mi')
let indexSumList = []
let accLen = 0
for (let i = 0; i < kwSpiltBySpace.length; i++) { // 进行求索引表达式的数组的构造,为求和做准备
if (kwSpiltBySpace[i] == '') continue
if (i > 0) accLen += kwSpiltBySpace[i - 1].length
indexSumList.push($.indexOfCP(['$word', kwSpiltBySpace[i], accLen]))
// console.log('$.indexOfCP([\'$word\',', kwSpiltBySpace[i], ',', accLen, '])', $.indexOfCP(['$word', kwSpiltBySpace[i], accLen]))
}
// console.log('indexSumList:', indexSumList)
let res = await db.collection(DBname).aggregate()
.match({
word: exp,
})
.project({
_id: false,
word: true,
word_id: true,
translation: true,
// indexsum: $.sum([$.indexOfCP(['$word', 's', 2]), $.indexOfCP(['$word', 't', 3])])
indexSum: $.sum(indexSumList)
})
.sort({
indexSum: 1,
word_id: 1
})
.skip(skip)
.limit(recordLimit)
.project({
indexSum: false,
})
.end()
console.log('res.list', res.list)
ctx.body = {
...rescontent.SUCCESS, data: {
lemmaSearch: [],
directSearch: res.list
}
}
} else {
// 中文的情况,直接按照有空格处理
// 将空格换为任意位数通配符进行匹配,同时允许空格切分的中文前后顺序不同
// 获取由空格分割的每个部分的第一个次出现位置索引并求和
let kwSpiltBySpace = keyword.split(' ')
kwSpiltBySpace = kwSpiltBySpace.filter(subStr => subStr.length > 0)
// 因为释义关键词前后顺序不定,故生成所有排列组合并构造正则表达式
let kwSpiltBySpaceAllList = get_all_sort_list.getAllSortList(kwSpiltBySpace.concat(), kwSpiltBySpace.length, true)
let expList = []
for (let k = 0; k < kwSpiltBySpaceAllList.length; k++) {
let expStr = '.*' + kwSpiltBySpaceAllList[k].join('.*') + '.*'
let exp = new RegExp(expStr, 'mi')
expList.push(exp)
}
// 动态生成 各部分出现次数求和 以及 第一次出现位置的索引的和 的待求和数组
let numSumList = []
let indexSumList = []
for (let i = 0; i < kwSpiltBySpace.length; i++) {
if (kwSpiltBySpace[i] == '') continue
numSumList.push($.subtract([$.size($.split(['$translation', kwSpiltBySpace[i]])), 1]))
indexSumList.push($.indexOfCP(['$translation', kwSpiltBySpace[i]]))
}
let res = await db.collection(DBname)
.aggregate()
.match({
// translation: _.or([/.*棒.*球.*/, /.*球.*棒.*/]),
translation: _.or(expList),
})
.project({
_id: false,
word: true,
word_id: true,
translation: true,
// numSum: $.sum([$.size($.split(['$translation', '棒'])), $.size($.split(['$translation', '球']))]),
// indexSum: $.sum([$.indexOfCP(['$translation', '棒']), $.split(['$translation', '球'])])
numSum: $.sum(numSumList),
indexSum: $.sum(indexSumList),
})
.sort({
numSum: -1,
indexSum: 1,
word_id: 1
})
.skip(skip)
.limit(recordLimit)
.project({
indexSum: 0,
numSum: 0,
})
.end()
console.log('res.list', res.list)
ctx.body = {
...rescontent.SUCCESS, data: {
lemmaSearch: [],
directSearch: res.list
}
}
}
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('getwordDetail', async (ctx, next) => {
let word_id = event.word_id
let user_id = event.user_id
let DBname = (word_id > 29999) ? 'word_all' : 'word'
try {
let res = await db.collection(DBname)
.aggregate()
.match({
word_id
})
.lookup({ // lookup-1,查找该单词是否在对应用户的生词本中
from: 'notebook',
let: {
wordId: '$word_id',
},
pipeline: $.pipeline()
.match(_.expr($.and([
$.eq(['$user_id', user_id]),
$.eq(['$word_id', '$$wordId']),
])))
.done(),
as: 'in_notebook'
})
.lookup({ // lookup-1,查找该词的所有tag及tag名字
from: 'word_in_book',
let: {
wd_id: '$word_id'
},
pipeline: $.pipeline() // 一级lookup,查找该词的所有tag
.match(_.expr($.eq(['$word_id', '$$wd_id'])))
.project({
_id: 0,
wd_bk_id: 1
})
.lookup({ // 二级lookup,查找每个tag的对应名字
from: 'word_book',
localField: 'wd_bk_id',
foreignField: 'wd_bk_id',
as: 'book'
})
.replaceRoot({
newRoot: $.mergeObjects([$.arrayElemAt(['$book', 0]), '$$ROOT'])
})
.project({
_id: 0,
wd_bk_id: 1,
tag: 1,
name: 1
})
.done(),
as: 'tagList',
})
.project({
_id: 0,
strip_word: 0,
})
.end()
console.log(res)
if (res.list[0].in_notebook.length > 0) {
res.list[0].in_notebook = true
} else {
res.list[0].in_notebook = false
}
if (res.list.length != 1) {
ctx.body = { ...rescontent.DATAERR }
} else {
ctx.body = { ...rescontent.SUCCESS, data: res.list[0] }
}
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('getBasicLearningData', async (ctx, next) => {
let user_id = event.user_id
let wd_bk_id = event.wd_bk_id
// console.log(event)
try {
// 获取未学数量(此方案较慢,通过两个同步执行的查询替换,已废弃)
// let needToLearnRes = db.collection('word_in_book')
// .aggregate()
// .match({ // 从词书与单词的关系表里获取当前学习的书的所有单词
// wd_bk_id: wd_bk_id
// })
// .lookup({ // lookup-1,从学习记录中匹配学过的单词
// from: 'learning_record',
// let: {
// wordId: '$word_id',
// },
// pipeline: $.pipeline()
// .match(_.expr($.and([
// $.eq(['$user_id', user_id]),
// $.eq(['$word_id', '$$wordId']),
// ])))
// .done(),
// as: 'word_list'
// })
// .match(_.expr( // 删去已经学过的单词(之前的lookup未匹配到说明没有学过)
// $.eq([$.size('$word_list'), 0]),
// ))
// // .project({
// // _id: 1
// // })
// .count('numToLearn')
// .end()
// console.log('needToLearnRes', needToLearnRes)
// {list:[{needTolearn:xxx}]}
let learnedNumRes = db.collection('learning_record')
.aggregate()
.match({ // 从词书与单词的关系表里获取当前学习的书的所有单词
user_id: user_id,
})
.lookup({ // lookup-1,从学习记录中匹配学过的单词
from: 'word_in_book',
let: {
wordId: '$word_id',
},
pipeline: $.pipeline()
.match(_.expr($.and([
$.eq(['$wd_bk_id', wd_bk_id]),
$.eq(['$word_id', '$$wordId']),
])))
.done(),
as: 'word_list'
})
.match(_.expr( // 删去已经学过的单词(之前的lookup未匹配到说明没有学过)
$.eq([$.size('$word_list'), 1]),
))
.count('learned')
.end()
// {list:[{learned:xxx}]}
let totalnumRes = db.collection('word_in_book')
.aggregate()
.match({
wd_bk_id: wd_bk_id
})
.count('total')
.end()
// {list:[{total:xxx}]}
let timeStamp = new Date().getTime()
let needToReviewRes = db.collection('learning_record')
// .where({ // 选取复习时间不晚于今天的所有记录
// user_id: user_id,
// master: false,
// next_l: _.lte(timeStamp),
// })
// .count()
.aggregate()
.match({ // 选取复习时间不晚于今天的所有记录
user_id: user_id,
master: false,
next_l: _.lte(timeStamp),
})
.count('numToReview')
.end()
// console.log('needToReviewRes', needToReviewRes)
// {list:[{numToReview:xxx}]}
// let resList = [needToLearnRes, needToReviewRes]
let resList = await Promise.all([learnedNumRes, totalnumRes, needToReviewRes])
// console.log(resList)
let total = 0
let learned = 0
let numToReview = 0
if (resList[1].list.length > 0 && resList[1].list[0].total >= 0) total = resList[1].list[0].total
if (resList[0].list.length > 0 && resList[0].list[0].learned >= 0) learned = resList[0].list[0].learned
if (resList[2].list.length > 0 && resList[2].list[0].numToReview >= 0) numToReview = resList[2].list[0].numToReview
let nums = {
needToLearn: total - learned,
needToReview: numToReview,
// needToReview: resList[1].total,
}
ctx.body = { ...rescontent.SUCCESS, data: nums }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('getLearningData', async (ctx, next) => {
const wd_bk_id = event.wd_bk_id
const user_id = event.user_id
let groupSize = event.groupSize
const getSize = Math.round(groupSize * 1.5)
const batchTimes = Math.ceil(getSize / 10)
const sampleSize = event.sample ? 9 : 0
try {
let tasks = []
for (let i = 0; i < batchTimes; i++) {
let num = i * 10 + 10 > getSize ? getSize - (i * 10) : 10
let promise = db.collection('word_in_book')
.aggregate()
.match({ // 从词书与单词的关系表里获取当前学习的书的所有单词
wd_bk_id: wd_bk_id
})
.sort({
wd_index: 1,
})
.lookup({ // lookup-1,从学习记录中匹配学过的单词
from: 'learning_record',
let: {
wordId: '$word_id',
},
pipeline: $.pipeline()
.match(_.expr($.and([
$.eq(['$user_id', user_id]),
$.eq(['$word_id', '$$wordId']),
])))
.done(),
as: 'word_list'
})
.match(_.expr( // 删去已经学过的单词(之前的lookup未匹配到说明没有学过)
$.eq([$.size('$word_list'), 0]),
))
.project({
_id: 0,
word_list: 0,
})
.skip(i * 10)
.limit(num)
.lookup({ // lookup-2,查找获取取得的单词是否在对应用户的生词本中
from: 'notebook',
let: {
wordId: '$word_id',
},
pipeline: $.pipeline()
.match(_.expr($.and([
$.eq(['$user_id', user_id]),
$.eq(['$word_id', '$$wordId']),
])))
.done(),
as: 'nb_record'
})
.lookup({ // lookup-3,查找获取取得的单词是否有学习过的“缓存”
from: 'learning_record_temp',
let: {
wordId: '$word_id',
},
pipeline: $.pipeline()
.match(_.expr($.and([
$.eq(['$user_id', user_id]),
$.eq(['$word_id', '$$wordId']),
])))
.done(),
as: 'l_r_temp_list'
})
.lookup({ // lookup-4,获取取得的单词的详细数据
from: 'word',
localField: 'word_id',
foreignField: 'word_id',
as: 'word_list'
})
.replaceRoot({ // 把单词详情合并到对象属性中
newRoot: $.mergeObjects([$.arrayElemAt(['$word_list', 0]), '$$ROOT'])
})
.project({
_id: 0,
word_id: 1,
word: 1,
translation: 1,
phonetic: 1,
in_notebook: $.gte([$.size('$nb_record'), 1]),
learning_record: $.arrayElemAt(['$l_r_temp_list', 0])
})
.lookup({ // lookup-5 在同一本词书中为每个单词随机取9个词做释义干扰项
from: 'word_in_book',
let: {
wordId: '$word_id',
},
pipeline: $.pipeline() // 一级lookup,筛选同本词书且word_id不同的词
.match({
wd_bk_id: wd_bk_id,
word_id: _.neq('$$wordId'),
})
.sample({ // 随机取出9个单词(做干扰项)
size: sampleSize
})
.lookup({ // 二级lookup,为取出的单词查找单词详细信息
from: 'word',
localField: 'word_id',
foreignField: 'word_id',
as: 'word_list',
})
.replaceRoot({ // 把单词详情合并到samplelist每个成员的对象属性中
newRoot: $.mergeObjects([$.arrayElemAt(['$word_list', 0]), '$$ROOT'])
})
.project({
_id: 0,
word_id: 1,
word: 1,
translation: 1,
})
.done(),
as: 'sample_list'
})
.end()
tasks.push(promise)
}
// 等所有批次返回结果后处理
let res = (await Promise.all(tasks)).reduce((acc, currentValue, index) => {
// console.log(acc)
acc.data = acc.data.concat(currentValue.list)
let wordIdList = []
for (let m = 0; m < currentValue.list.length; m++) {
if (currentValue.list[m].learning_record) {
wordIdList.push(currentValue.list[m].word_id)
}
}
acc.wordIdList = acc.wordIdList.concat(wordIdList)
// console.log(currentValue)
return acc
}, { data: [], wordIdList: [] })
// 删除取出来的临时记录
if (res.wordIdList.length > 0) {
let res1 = await db.collection('learning_record_temp')
.where({
user_id,
word_id: _.in(res.wordIdList)
})
.remove()
console.log('remove list', res.wordIdList, ' for user', user_id)
console.log(res1)
}
ctx.body = { ...rescontent.SUCCESS, data: res.data }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('getReviewData', async (ctx, next) => {
const wd_bk_id = event.wd_bk_id
const user_id = event.user_id
let groupSize = event.groupSize
const batchTimes = Math.ceil(groupSize / 10)
const sampleSize = event.sample ? 9 : 0
try {
let tasks = []
let timeStamp = new Date().getTime()
for (let i = 0; i < batchTimes; i++) {
let num = i * 10 + 10 > groupSize ? groupSize - (i * 10) : 10
let promise = db.collection('learning_record')
.aggregate()
.match({ // 选取复习时间不晚于今天的所有记录
user_id: user_id,
master: false,
next_l: _.lte(timeStamp),
})
.sort({
next_l: 1,
})
.skip(i * 10)
.limit(num)
.lookup({ // lookup-1,查找获取取得的单词是否在对应用户的生词本中
from: 'notebook',
let: {
wordId: '$word_id',
},
pipeline: $.pipeline()
.match(_.expr($.and([
$.eq(['$user_id', user_id]),
$.eq(['$word_id', '$$wordId']),
])))
.done(),
as: 'nb_record'
})
.lookup({ // lookup-2,获取取得的单词的详细数据
from: 'word',
localField: 'word_id',
foreignField: 'word_id',
as: 'word_list'
})
.replaceRoot({ // 把单词详情合并到对象属性中
newRoot: $.mergeObjects([$.arrayElemAt(['$word_list', 0]), '$$ROOT'])
})
.addFields({
in_notebook: $.eq([$.size('$nb_record'), 1]),
record: {
EF: '$EF',
NOI: '$NOI',
last_l: '$last_l',
next_l: '$next_l',
master: '$master',
word_id: '$word_id',
next_n: '$next_n',
},
})
.project({
_id: 0,
word_id: 1,
word: 1,
translation: 1,
phonetic: 1,
in_notebook: 1,
record: 1
})
.lookup({ // lookup-3 在同一本词书中为每个单词随机取9个词做释义干扰项
from: 'word_in_book',
let: {
wordId: '$word_id',
},
pipeline: $.pipeline() // 一级lookup,筛选同本词书且word_id不同的词
.match({
wd_bk_id: wd_bk_id,
word_id: _.neq('$$wordId'),
})
.sample({ // 随机取出9个单词(做干扰项)
size: sampleSize
})
.lookup({ // 二级lookup,为取出的单词查找单词详细信息
from: 'word',
localField: 'word_id',
foreignField: 'word_id',
as: 'word_list',
})
.replaceRoot({ // 把单词详情合并到samplelist每个成员的对象属性中
newRoot: $.mergeObjects([$.arrayElemAt(['$word_list', 0]), '$$ROOT'])
})
.project({
_id: 0,
word_id: 1,
word: 1,
translation: 1,
})
.done(),
as: 'sample_list'
})
.end()
tasks.push(promise)
}
// 等所有批次返回结果后处理
let res = (await Promise.all(tasks)).reduce((acc, currentValue, index) => {
// console.log(acc)
acc = acc.concat(currentValue.list)
// console.log(currentValue)
return acc
}, [])
ctx.body = { ...rescontent.SUCCESS, data: res }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('toggleAddToNB', async (ctx, next) => {
const user_id = event.user_id
const word_id = event.word_id
try {
let res = undefined
if (event.add) {
res = await db.collection('notebook')
.add({
data: {
user_id,
word_id,
c_time: new Date().getTime()
}
})
console.log(res)
} else {
res = await db.collection('notebook')
.where({
user_id,
word_id,
})
.remove()
console.log(res)
}
let correctMsg = event.add ? "collection.add:ok" : "collection.remove:ok"
if (res.errMsg == correctMsg) {
ctx.body = { ...rescontent.SUCCESS, data: true }
} else {
ctx.body = { ...rescontent.DBERR, data: false, err: res }
}
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('addLearningRecord', async (ctx, next) => {
const user_id = event.user_id
let wordLearningRecord = event.learnedRecord
let learningRecord = event.learningRecord
const batchTimesForLearned = Math.ceil(wordLearningRecord.length / 10)
let batchTimesForLearning = 0
if (learningRecord) batchTimesForLearning = Math.ceil(learningRecord.length / 10)
let now = new Date()
now.setMilliseconds(0)
now.setSeconds(0)
now.setMinutes(0)
now.setHours(0)
let last_l = now.getTime()
let next_l = last_l + 86400000
// 检查属性,means允许自定义
for (let i = 0; i < wordLearningRecord.length; i++) {
if (wordLearningRecord[i].last_l === undefined) wordLearningRecord[i].last_l = last_l
if (wordLearningRecord[i].next_l === undefined) wordLearningRecord[i].next_l = next_l
if (wordLearningRecord[i].NOI === undefined) wordLearningRecord[i].NOI = 1
if (wordLearningRecord[i].EF === undefined) wordLearningRecord[i].EF = '2.5'
if (wordLearningRecord[i].next_n === undefined) wordLearningRecord[i].next_n = 0
if (wordLearningRecord[i].master === undefined) wordLearningRecord[i].master = false
if (wordLearningRecord[i].c_time === undefined) wordLearningRecord[i].c_time = last_l
}
console.log(wordLearningRecord)
try {
// 将完成学习的单词加入学习记录数据库(learning_record)
let learnedRes = []
for (let i = 0; i < batchTimesForLearned; i++) {
// 承载所有读操作的 promise 的数组
let tasks = []
let start = i * 10
let end = ((start + 10) > wordLearningRecord.length) ? wordLearningRecord.length : (start + 10)
// 等待所有
for (let j = start; j < end; j++) {
wordLearningRecord[j].user_id = user_id
let promise = db.collection('learning_record')
.add({
data: wordLearningRecord[j]
})
tasks.push(promise)
}
let resInner = (await Promise.all(tasks)).reduce((acc, currentValue, index) => {
acc[index] = currentValue._id
// console.log(cur._id)
return acc
}, [])
console.log('learned record batch', i, 'done')
console.log('learned record batch', i, ':', resInner)
learnedRes = learnedRes.concat(resInner)
}
// 下面更新daily_sum对应数据
let addNum = 0
for (let k = 0; k < learnedRes.length; k++) {
if (learnedRes[k] && learnedRes[k] != '') addNum++
}
let updateDailySumRes = await db.collection('daily_sum')
.where({
user_id,
date: last_l,
})
.update({
data: {
learn: _.inc(addNum)
}
})
if (updateDailySumRes.stats.updated != 1) {
let createDailySumRes = await db.collection('daily_sum')
.add({
data: {
user_id,
date: last_l,
learn: addNum,
review: 0,
l_time: 0,
}
})
if (createDailySumRes._id && createDailySumRes._id != '') {
console.log('createDailySumRes for user', user_id, 'successfully')
}
}
// 将完成学习的单词加入临时记录的数据库(learning_record)
let tempRes = []
if (batchTimesForLearning > 0) {
for (let m = 0; m < batchTimesForLearning; m++) {
// 承载所有读操作的 promise 的数组
let tasks = []
let start = m * 10
let end = ((start + 10) > learningRecord.length) ? learningRecord.length : (start + 10)
// 等待所有
for (let n = start; n < end; n++) {
learningRecord[n].user_id = user_id
let promise = db.collection('learning_record_temp')
.add({
data: learningRecord[n]
})
tasks.push(promise)
}
let resInner = (await Promise.all(tasks)).reduce((acc, currentValue, index) => {
acc[index] = currentValue._id
// console.log(cur._id)
return acc
}, [])
console.log('learning recordbatch', m, 'done')
console.log('learning recordbatch', m, ':', resInner)
tempRes = tempRes.concat(resInner)
}
}
ctx.body = { ...rescontent.SUCCESS, data: { learnedRes, tempRes } }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
app.router('updateLearningRecord', async (ctx, next) => {
const user_id = event.user_id
const wordLearningRecord = event.wordLearningRecord
const batchTimes = Math.ceil(wordLearningRecord.length / 10)
try {
// 先获取用户的OF矩阵
let userRes = await db.collection('learner')
.where({
user_id
})
.field({
of_matrix: true,
})
.get()
console.log('userRes', userRes)
let of_matrix = userRes.data[0].of_matrix
// console.log(of_matrix)
let res = []
let updateNum = 0
for (let i = 0; i < batchTimes; i++) {
// 承载所有读操作的 promise 的数组
let tasks = []
let start = i * 10
let end = ((start + 10) > wordLearningRecord.length) ? wordLearningRecord.length : (start + 10)
// 等待所有
for (let j = start; j < end; j++) {
let result = sm_5_js.sm_5(of_matrix, wordLearningRecord[j])
let record = result.wd_learning_record
wordLearningRecord[j].newNOI = record.NOI
wordLearningRecord[j].newMaster = record.master
of_matrix = result.OF
record.user_id = user_id
let promise = db.collection('learning_record')
.where({
user_id,
word_id: wordLearningRecord[j].word_id
})
.update({
data: record
})
tasks.push(promise)
}
// 更新of_矩阵
let updateUserPromiseIndex = -1
if (i == batchTimes - 1) {
let updateUserPromise = db.collection('learner')
.where({
user_id
})
.update({
data: {
of_matrix: _.set(of_matrix)
}
})
tasks.push(updateUserPromise)
updateUserPromiseIndex = tasks.length - 1
}
let resInner = (await Promise.all(tasks)).reduce((acc, currentValue, index) => {
if (updateUserPromiseIndex != -1 && index == updateUserPromiseIndex) {
console.log('update of_matrix result', currentValue)
} else if (currentValue.stats.updated > 0) {
acc[index] = {
word_id: wordLearningRecord[index].word_id,
NOI: wordLearningRecord[index].newNOI,
master: wordLearningRecord[index].newMaster,
updated: currentValue.stats.updated,
success: true,
}
updateNum++
} else {
acc[index] = {
word_id: wordLearningRecord[index].word_id,
updated: currentValue.stats.updated,
success: false,
}
}
return acc
}, [])
console.log('batch', i, 'done')
console.log('batch', i, ':', resInner)
res = res.concat(resInner)
}
// 下面更新daily_sum对应数据
let now = new Date()
now.setMilliseconds(0)
now.setSeconds(0)
now.setMinutes(0)
now.setHours(0)
let date = now.getTime()
let updateDailySumRes = await db.collection('daily_sum')
.where({
user_id,
date,
})
.update({
data: {
review: _.inc(updateNum)
}
})
if (updateDailySumRes.stats.updated != 1) {
let createDailySumRes = await db.collection('daily_sum')
.add({
data: {
user_id,
date,
learn: 0,
review: updateNum,
l_time: 0,
}
})
if (createDailySumRes._id && createDailySumRes._id != '') {
console.log('createDailySumRes for user', user_id, 'successfully')
}
}
ctx.body = { ...rescontent.SUCCESS, data: res }
} catch (e) { // 抛出错误
console.error(e)
ctx.body = { ...rescontent.DBERR, err: e }
}
})
return app.serve()
}
================================================
FILE: cloudfunctions/wordRouter/package.json
================================================
{
"name": "wordRouter",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"tcb-router": "^1.1.2",
"wx-server-sdk": "~2.5.3",
"bent": "<=7.3.12"
}
}
================================================
FILE: cloudfunctions/wordRouter/utils/format_time.js
================================================
// 传入时间的毫秒数(date.getTime())获取时间详情
const formatTime = (time) => {
var date = new Date(time)
var y = date.getFullYear()
var m = date.getMonth() + 1
var d = date.getDate()
var h = date.getHours()
var min = date.getMinutes()
var s = date.getSeconds()
var timeStr = y + "-" + enterZero(m) + "-" + enterZero(d) + " " + enterZero(h) + ":" + enterZero(min) + ":" + enterZero(s)
return timeStr
}
const formatDate = (time) => {
var date = new Date(time)
var y = date.getFullYear()
var m = date.getMonth() + 1
var d = date.getDate()
var dateStr = y + "-" + enterZero(m) + "-" + enterZero(d)
return dateStr
}
const dateNum = (time) => {
var date = new Date(time)
var y = date.getFullYear()
var m = date.getMonth() + 1
var d = date.getDate()
var num = y *10000 + m*100 + d
return num
}
const enterZero = (num) => {
num = Math.abs(num)
if (num <= 9) {
num = "0" + num
}
return num
}
module.exports = {
formatTime: formatTime,
formatDate: formatDate,
dateNum: dateNum,
}
================================================
FILE: cloudfunctions/wordRouter/utils/get_all_sort_list.js
================================================
/**
*
* @param {*} source 源数组
* @param {*} count 要取出多少项
* @param {*} isPermutation 是否使用排列的方式
* @return {any[]} 所有排列组合,格式为 [ [1,2], [1,3]] ...
*/
const getAllSortList = (source, count, isPermutation = true) => {
//如果只取一位,返回数组中的所有项,例如 [ [1], [2], [3] ]
let currentList = source.map((item) => [item]);
if (count === 1) {
return currentList;
}
let result = [];
//取出第一项后,再取出后面count - 1 项的排列组合,并把第一项的所有可能(currentList)和 后面count-1项所有可能交叉组合
for (let i = 0; i < currentList.length; i++) {
let current = currentList[i];
//如果是排列的方式,在取count-1时,源数组中排除当前项
let children = [];
if (isPermutation) {
children = getAllSortList(source.filter(item => item !== current[0]), count - 1, isPermutation);
}
//如果是组合的方法,在取count-1时,源数组只使用当前项之后的
else {
children = getAllSortList(source.slice(i + 1), count - 1, isPermutation);
}
for (let child of children) {
result.push([...current, ...child]);
}
}
return result;
}
// let arr = [1, 2, 3];
// const result = getNumbers(arr, 2, false);
// console.log(result);
// //[ [ 1, 2 ], [ 1, 3 ], [ 2, 3 ] ]
// const result2 = getNumbers(arr, 2);
// console.log(result2);
// //[ [ 1, 2 ], [ 1, 3 ], [ 2, 1 ], [ 2, 3 ], [ 3, 1 ], [ 3, 2 ] ]
module.exports = {
getAllSortList: getAllSortList,
}
================================================
FILE: cloudfunctions/wordRouter/utils/response_content.js
================================================
const SUCCESS = { errorcode: 100, errormsg: "success" } //成功
const LOGINOK = { errorcode: 1, errormsg: "Login successfully" } //登录成功
const REGISTEROK= { errorcode: 2, errormsg: "Register successfully" } //注册成功
const DBERR = { errorcode: -1, errormsg: "Database error!" } //数据库操作失败
const ROUTERERR = { errorcode: -2, errormsg: "Wrong router name" } //路由名字有误
const LOGINERR = { errorcode: -3, errormsg: "Wrong username or pwd" } //登录信息有误
const DATAERR = { errorcode: -4, errormsg: "Wrong data!" } //数据有误
const UNKOWNERR = { errorcode: -100, errormsg: "Unkown error!" } //出现未知错误
module.exports={
SUCCESS: SUCCESS,
LOGINOK: LOGINOK,
REGISTEROK: REGISTEROK,
DBERR: DBERR,
ROUTERERR: ROUTERERR,
LOGINERR: LOGINERR,
DATAERR: DATAERR,
UNKOWNERR: UNKOWNERR,
}
================================================
FILE: cloudfunctions/wordRouter/utils/sm-5.js
================================================
// SM-5算法
// 计算下一个最优间隔的同时更新OF矩阵,从而单词在学习的时候不是一个个体,而是
// 用于生成最佳区间的随机散布 NOI--near-optimal intervals
// -------------------------------------------------------------
// 优点1: 通过一些差异值来加速OF矩阵优化过程
// 优点2: 消除复习的块状问题,将同一时期学习的内容适当分散进行复习
// 公式: NOI=PI+(OI-PI)*(1+m) m∈(-0.5, 0.5)
// m需满足(设概率密度函数为f(x)):
// (0, 0.5)内的概率为0.5,即 ∫[0, 0.5]f(x)dx=0.5
// m=0的概率为m=0.5的概率的100倍 即 f(0)/f(0.5)=100
// 假设概率密度函数为 f(x)=a*exp(-b*x)
// -------------------------------------------------------------
// Piotr Wozniak求得 a=0.047; b=0.092;
// 从0到m的积分记为概率p,对于每一个p都有一个对应的m存在,p∈(0, 0.5)
// 生成一个(0, 1)之间的随机数,减去0.5得p,则|p|∈(0, 0.5),而p的符号可以控制m的符号
// 则 ∫[0, m]f(x)dx=|p| => ∫[0, m]d( a*exp(-b*x) / (-b) )=|p| => m=-1/b*ln(1-b/a*|p|))
//
// const createNOI = (PI, OI) => {
// let a = 0.047
// let b = 0.092
// let randNum = Math.random()
// let p = randNum - 0.5
// console.log('random p', p)
// let m = -1 / b * (Math.log((1 - b / a * Math.abs(p))))
// m = m * Math.sign(p)
// console.log('random m', m)
// let NOI = PI + (OI - PI) * (1 + m)
// NOI = Math.round(NOI)
// return NOI
// }
// -------------------------------------------------------------
// 由于作者给出的参数带入是有误的,采用类正态分布实现分布函数
// 原型(标准正态分布):f(x) = 1/(√(2π)*Ω) * e(-x^2/(2Ω^2))
// 简化:f(x) = a*e^(-b*x^2)
// f(0) = 100*f(0.5) 可求得 b = -18.420680743952367
// ∫[0, 0.5]f(x)dx = 0.5 可求得 a = 2.4273047133848933
// 积分计算器网址: https://zh.numberempire.com/definiteintegralcalculator.php
// 画函数图像网址:https://www.desmos.com/calculator?lang=zh-CN
// 这里使用能解正态分布分位数的库进行运算
// f(0) = 100*f(0.5) 按正态分布算,可求得 std=0.1647525572455652
// X ~ N(0,0.1647525572455652) 从0~0.5的累计分布值为0.4987967402705885
// 故若要满足∫[0, 0.5]f(x)dx = 0.5,要在前面再乘上
// JStat库的jStat.normal.inv( p, mean, std )可以求出N(mean,std)分布从负无穷开始累计分布为p的分位点
// 因此思路转变为,首先随机获取[0, 1)的数r, r-0.5得到[-0.5, 0.5)的数m,(m*0.4987967402705885/0.5+0.5)得到累计值
// 即jStat.normal.inv(abs(m*0.4987967402705885/0.5)+0.5, 0, 0.1647525572455652) 可得到分位点
const jStat = require("./jstat.min.js")
const createNOI = (PI, OI) => {
let mean = 0
let std = 0.1647525572455652
let randNum = Math.random()
// console.log('randNum', randNum)
let p = Math.abs((randNum - 0.5) * 0.4987967402705885 / 0.5) + 0.5
// console.log('random p', p)
let inv_cdf = jStat.normal.inv(p, mean, std)
let m = inv_cdf * Math.sign(randNum - 0.5)
// console.log('random m', m)
let NOI = PI + (OI - PI) * (1 + m)
NOI = Math.round(NOI)
return NOI
}
// 符号函数
const sgn = (num) => {
if (num < 0) {
return -1
} else if (num == 0) {
return 0
} else {
return 1
}
}
// 计算新的OF矩阵对应项
// 输入:
// last_i - 用于相关项目的最后(上一个)间隔(原文描述为the last interval used for the item in question)
// q - 重复响应的质量
// used_OF - 用于计算相关项目的最后一个间隔时使用的最佳因子
// old_OF - 与项目的相关重复次数和电子因子相对应的 OF 条目的前一个值
// fraction - 属于确定修改速率的范围 (0,1) 的数字 (OF矩阵的变化越快)
// 输出:
// new_OF - 考虑的 OF 矩阵条目的新计算值
// 局部变量:
// modifier - 确定 OF 值将增加或减少多少次的数字
// mod5 - 在 q=5 的情况下为修饰符建议的值
// mod2 - 在 q=2 的情况下为修饰符建议的值
const calculateNewOF = (last_i, q, used_OF, old_OF, fraction = 0.8) => {
let modifier
let mod5 = (last_i + 1) / last_i
if (mod5 < 1.05) mod5 = 1.05
let mod2 = (last_i - 1) / last_i
if (mod2 > 0.75) mod2 = 0.75
if (q > 4) {
modifier = 1 + (mod5 - 1) * (q - 4)
} else {
modifier = 1 - (1 - mod2) / 2 * (4 - q)
}
if (modifier < 0.05) modifier = 0.05
let new_OF = used_OF * modifier
if (q > 4) if (new_OF < old_OF) new_OF = old_OF
if (q < 4) if (new_OF > old_OF) new_OF = old_OF
new_OF = new_OF * fraction + old_OF * (1 - fraction)
if (new_OF < 1.2) new_OF = 1.2
new_OF = new_OF.toFixed(4)
new_OF = parseFloat(new_OF)
return new_OF
}
// 单词记录提供数据:循环次数,上次的EF,上次的间隔时间(/天), q(quality,回忆质量)
// 其他:OF矩阵
const sm_5 = (OF, wd_learning_record) => {
let EF = wd_learning_record.EF
let q = wd_learning_record.q
let last_NOI = wd_learning_record.NOI
let n = wd_learning_record.next_n
let last_l = wd_learning_record.last_l
let next_l = wd_learning_record.next_l
let master = wd_learning_record.master
if (master) {
return {
wd_learning_record: {
word_id: wd_learning_record.word_id,
last_l,
next_l,
NOI: last_NOI,
EF,
next_n: n,
master,
},
OF,
}
}
// 计算此时与上次复习/学习的时间差(/天)
let now = new Date()
now.setMilliseconds(0)
now.setSeconds(0)
now.setMinutes(0)
now.setHours(0)
let last_i = Math.ceil((now.getTime() - last_l) / 86400000)
// console.log('word', wd_learning_record.word_id, 'last interval', last_i)
// 更改EF(由于作为键,EF规定为一位小数转换成的字符串)
EF = parseFloat(EF) + (0.1 - (5 - q) * (0.08 + (5 - q) * 0.02))
if (EF < 1.3) EF = 1.3
if (EF > 2.8) EF = 2.8
EF = EF.toFixed(1)
// 更改矩阵对应项,这里认为若实际间隔时间超过所需间隔时间的1.5倍
// 则视为极大异常值,规整为1.5倍,且不更改矩阵
let used_OF = OF[EF][n - 1]
if (!used_OF) used_OF = 1.2
n++
if (!OF[EF][n - 1]) OF[EF][n - 1] = 1.2
if (last_i <= 1.5 * last_NOI) {
let old_OF = OF[EF][n - 1]
let new_OF = calculateNewOF(last_i, q, used_OF, old_OF)
// console.log('new_OF of', 'OF[', EF, '][', n - 1, ']:', new_OF)
OF[EF][n - 1] = new_OF
} else {
// console.log('last_i', last_i, 'is longer than 1.5 expected interval :', last_NOI)
last_i = Math.round(last_NOI * 1.5)
}
// 计算最优间隔时长并进行指定分布的随机分散
// 同时计算下次需要复习的时间(1970.1.1至今毫秒数表示)
let NOI
if (q < 2) {
n = 0
NOI = 1
} else if (q < 3) {
n = 1
let interval = OF[EF][0]
NOI = Math.round(interval)
} else {
let interval = n == 1 ? 5 : OF[EF][n - 1] * last_i
// 若下个最优间隔时间大于100天,则将单词标记为已掌握
if (interval > 100) master = true
console.log('next optimal interval', interval)
NOI = Math.round(createNOI(last_i, interval))
if (NOI > 100 && !master) NOI = 100
if (NOI < 0 && !master) NOI = 1
}
last_l = now.getTime()
next_l = last_l + NOI * 86400000
return {
wd_learning_record: {
word_id: wd_learning_record.word_id,
last_l,
next_l,
NOI,
EF,
next_n: n,
master,
},
OF,
}
}
module.exports = {
sm_5: sm_5,
}
================================================
FILE: miniprogram/app.js
================================================
// app.js
const rescontent = require('./utils/response_content.js')
const { formatTime } = require('./utils/format_time.js')
const userApi = require("./utils/userApi.js")
App({
onLaunch: function () {
if (!wx.cloud) {
console.error('请使用 2.2.3 或以上的基础库以使用云能力');
} else {
wx.cloud.init({
// env 参数说明:
// env 参数决定接下来小程序发起的云开发调用(wx.cloud.xxx)会默认请求到哪个云环境的资源
// 此处请填入环境 ID, 环境 ID 可打开云控制台查看
// 如不填则使用默认环境(第一个创建的环境)
// env: 'my-env-id',
traceUser: true,
});
}
this.checkLogin()
wx.disableAlertBeforeUnload()
},
globalData: {
isLogin: false,
tryingLogin: true,
userInfo: {
// user_id: 2,
// l_book_id: 2,
settings: {
// learn_repeat_t: 3,
// group_size: 10,
// learn_first_m: 'chooseTrans',
// learn_second_m: 'recallTrans',
// learn_third_m: 'recallWord',
// learn_fourth_m: 'recallTrans',
// timing: true,
// timing_duration: 1000,
// autoplay: false,
// type: 1,
// review_repeat_t: 2,
// review_first_m: 'recallTrans',
// review_second_m: 'chooseTrans',
// review_second_m: 'recallWord',
// review_third_m: 'recallTrans',
}
},
updatedForIndex: false,
updatedForOverview: false,
forChangeAvatar: {
change: false,
tempImgSrc: '',
imgSrc: '',
}
},
checkLogin: async function () {
this.globalData.tryingLogin = true
// let history = wx.getStorageSync('history')
// wx.clearStorageSync()
// wx.setStorageSync('history', history)
// console.log('checkLogin')
// console.log('this.globalData.tryingLogin ', this.globalData.tryingLogin)
let storageContent = wx.getStorageSync('userInfo')
if (storageContent && (new Date().getTime() - storageContent.time) < 86400000 * 2) {
let res = await userApi.getUserInfoViaId({ user_id: storageContent.info.user_id })
if (res.errorcode == rescontent.SUCCESS.errorcode) {
this.globalData.isLogin = true
this.globalData.userInfo = res.data
let lastlogin = formatTime(res.data.last_login)
wx.showToast({
title: `自动登录成功,上次登录时间 ${lastlogin}`,
icon: 'none',
duration: 1500,
})
storageContent.info = res.data
wx.setStorageSync('userInfo', storageContent)
} else {
wx.showToast({
title: '自动登录失败,请重新登录',
icon: 'none',
duration: 1500,
})
wx.removeStorageSync('userInfo')
}
} else if (storageContent) {
wx.showToast({
title: '登录已过期,请重新登录',
icon: 'none',
duration: 1500,
})
wx.removeStorageSync('userInfo')
}
this.globalData.tryingLogin = false
// console.log('this.globalData.tryingLogin ', this.globalData.tryingLogin)
},
});
================================================
FILE: miniprogram/app.json
================================================
{
"pages": [
"pages/index/index",
"pages/user/user",
"pages/overview/overview",
"pages/login/login",
"pages/search/search",
"pages/word_detail/word_detail",
"pages/learning/learning",
"pages/review/review",
"pages/word_list/word_list",
"pages/image_cropper/image_cropper",
"pages/user_settings/user_settings"
],
"window": {
"backgroundColor": "#FFFFFF",
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#FFFFFF",
"navigationBarTitleText": "学不会单词",
"navigationBarTextStyle": "black"
},
"tabBar": {
"color": "#F0F0F0",
"backgroundColor": "#FFFFFF",
"selectedColor": "#6DAFFE",
"borderStyle": "white",
"position": "bottom",
"list": [
{
"pagePath": "pages/index/index",
"text": " ",
"iconPath": "static/images/tab-learn-CDCDCD.png",
"selectedIconPath": "static/images/tab-learn-A6D6FA.png"
},
{
"pagePath": "pages/overview/overview",
"text": " ",
"iconPath": "static/images/tab-overview-CDCDCD.png",
"selectedIconPath": "static/images/tab-overview-A6D6FA.png"
},
{
"pagePath": "pages/user/user",
"text": " ",
"iconPath": "static/images/tab-user-CDCDCD.png",
"selectedIconPath": "static/images/tab-user-A6D6FA.png"
}
]
},
"sitemapLocation": "sitemap.json",
"style": "v2",
"lazyCodeLoading": "requiredComponents"
}
================================================
FILE: miniprogram/app.wxss
================================================
/**app.wxss**/
@import './static/iconfont.wxss';
@import './static/color.wxss';
.container {
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
}
button {
background: initial;
}
button:focus {
outline: 0;
}
button::after {
border: none;
}
page {
background: #f6f6f6;
display: flex;
flex-direction: column;
justify-content: flex-start;
/* overflow: hidden; */
}
================================================
FILE: miniprogram/components/cloudTipModal/index.js
================================================
// miniprogram/components/cloudTipModal/index.js
const { isMac } = require('../../envList.js');
Component({
/**
* 页面的初始数据
*/
data: {
showUploadTip: false,
tipText: isMac ? 'sh ./uploadCloudFunction.sh' : './uploadCloudFunction.bat'
},
properties: {
showUploadTipProps: Boolean
},
observers: {
showUploadTipProps: function(showUploadTipProps) {
this.setData({
showUploadTip: showUploadTipProps
});
}
},
methods: {
onChangeShowUploadTip() {
this.setData({
showUploadTip: !this.data.showUploadTip
});
},
copyShell() {
wx.setClipboardData({
data: this.data.tipText,
});
},
}
});
================================================
FILE: miniprogram/components/cloudTipModal/index.json
================================================
{
"usingComponents": {},
"component": true
}
================================================
FILE: miniprogram/components/cloudTipModal/index.wxml
================================================
0?"right":"left":L>0?"left":"right"}var F=y.get("rotate");if(O="number"==typeof F?F*(Math.PI/180):F?L<0?-A+Math.PI:-A:0,o=!!O,p.x=I,p.y=T,p.rotation=O,p.setStyle({verticalAlign:"middle"}),R){p.setStyle({align:D});var G=p.states.select;G&&(G.x+=p.x,G.y+=p.y)}else{var H=p.getBoundingRect().clone();H.applyTransform(p.getComputedTransform());var W=(p.style.margin||0)+2.1;H.y-=W/2,H.height+=W,r.push({label:p,labelLine:f,position:v,len:S,len2:M,minTurnAngle:w.get("minTurnAngle"),maxSurfaceAngle:w.get("maxSurfaceAngle"),surfaceNormal:new ai(L,k),linePoints:C,textAlign:D,labelDistance:m,labelAlignTo:_,edgeDistance:x,bleedMargin:b,rect:H})}s.setTextConfig({inside:R})}})),!o&&t.get("avoidLabelOverlap")&&function(t,e,n,i,r,o,a,s){for(var l=[],u=[],h=Number.MAX_VALUE,c=-Number.MAX_VALUE,p=0;p i&&(i=e);var o=i%2?i+2:i+3;r=[];for(var a=0;a 0){var I=o(v)?s:l;v>0&&(v=v*S+w),_[x++]=I[M],_[x++]=I[M+1],_[x++]=I[M+2],_[x++]=I[M+3]*v*256}else x+=4}return c.putImageData(m,0,0),h},t.prototype._getBrush=function(){var t=this._brushCanvas||(this._brushCanvas=C()),e=this.pointSize+this.blurSize,n=2*e;t.width=n,t.height=n;var i=t.getContext("2d");return i.clearRect(0,0,n,n),i.shadowOffsetX=n,i.shadowBlur=this.blurSize,i.shadowColor="#000",i.beginPath(),i.arc(-e,e,this.pointSize,0,2*Math.PI,!0),i.closePath(),i.fill(),t},t.prototype._getGradient=function(t,e){for(var n=this._gradientPixels,i=n[e]||(n[e]=new Uint8ClampedArray(1024)),r=[0,0,0,0],o=0,a=0;a<256;a++)t[e](a/255,!0,r),i[o++]=r[0],i[o++]=r[1],i[o++]=r[2],i[o++]=r[3];return i},t}();function ok(t){var e=t.dimensions;return"lng"===e[0]&&"lat"===e[1]}var ak=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){var i;e.eachComponent("visualMap",(function(e){e.eachTargetSeries((function(n){n===t&&(i=e)}))})),this.group.removeAll(),this._incrementalDisplayable=null;var r=t.coordinateSystem;"cartesian2d"===r.type||"calendar"===r.type?this._renderOnCartesianAndCalendar(t,n,0,t.getData().count()):ok(r)&&this._renderOnGeo(r,t,i,n)},e.prototype.incrementalPrepareRender=function(t,e,n){this.group.removeAll()},e.prototype.incrementalRender=function(t,e,n,i){var r=e.coordinateSystem;r&&(ok(r)?this.render(e,n,i):this._renderOnCartesianAndCalendar(e,i,t.start,t.end,!0))},e.prototype._renderOnCartesianAndCalendar=function(t,e,n,i,r){var o,a,s,l,u=t.coordinateSystem;if(Nw(u,"cartesian2d")){var h=u.getAxis("x"),c=u.getAxis("y");0,o=h.getBandWidth(),a=c.getBandWidth(),s=h.scale.getExtent(),l=c.scale.getExtent()}for(var p=this.group,d=t.getData(),f=t.getModel(["emphasis","itemStyle"]).getItemStyle(),g=t.getModel(["blur","itemStyle"]).getItemStyle(),y=t.getModel(["select","itemStyle"]).getItemStyle(),v=ch(t),m=t.get(["emphasis","focus"]),_=t.get(["emphasis","blurScope"]),x=Nw(u,"cartesian2d")?[d.mapDimension("x"),d.mapDimension("y"),d.mapDimension("value")]:[d.mapDimension("time"),d.mapDimension("value")],b=n;bs[1]||I 1;)r=r.parentNode;var o=n.getColorFromPalette(r.name||r.dataIndex+"",e);return t.depth>1&&"string"==typeof o&&(o=Ue(o,(t.depth-1)/(i-1)*.5)),o}(r,t,i.root.height)),I(n.ensureUniqueItemVisual(r.dataIndex,"style"),o)}))}))}function Uk(t,e){return e=e||[0,0],O(["x","y"],(function(n,i){var r=this.getAxis(n),o=e[i],a=t[i]/2;return"category"===r.type?r.getBandWidth():Math.abs(r.dataToCoord(o-a)-r.dataToCoord(o+a))}),this)}function Xk(t,e){return e=e||[0,0],O([0,1],(function(n){var i=e[n],r=t[n]/2,o=[],a=[];return o[n]=i-r,a[n]=i+r,o[1-n]=a[1-n]=e[1-n],Math.abs(this.dataToPoint(o)[n]-this.dataToPoint(a)[n])}),this)}function Yk(t,e){var n=this.getAxis(),i=e instanceof Array?e[0]:e,r=(t instanceof Array?t[0]:t)/2;return"category"===n.type?n.getBandWidth():Math.abs(n.dataToCoord(i-r)-n.dataToCoord(i+r))}function Zk(t,e){return e=e||[0,0],O(["Radius","Angle"],(function(n,i){var r=this["get"+n+"Axis"](),o=e[i],a=t[i]/2,s="category"===r.type?r.getBandWidth():Math.abs(r.dataToCoord(o-a)-r.dataToCoord(o+a));return"Angle"===n&&(s=s*Math.PI/180),s}),this)}function jk(t,e,n,i){return t&&(t.legacy||!1!==t.legacy&&!n&&!i&&"tspan"!==e&&("text"===e||dt(t,"text")))}function qk(t,e,n){var i,r,o,a=t;if("text"===e)o=a;else{o={},dt(a,"text")&&(o.text=a.text),dt(a,"rich")&&(o.rich=a.rich),dt(a,"textFill")&&(o.fill=a.textFill),dt(a,"textStroke")&&(o.stroke=a.textStroke),r={type:"text",style:o,silent:!0},i={};var s=dt(a,"textPosition");n?i.position=s?a.textPosition:"inside":s&&(i.position=a.textPosition),dt(a,"textPosition")&&(i.position=a.textPosition),dt(a,"textOffset")&&(i.offset=a.textOffset),dt(a,"textRotation")&&(i.rotation=a.textRotation),dt(a,"textDistance")&&(i.distance=a.textDistance)}return Kk(o,t),P(o.rich,(function(t){Kk(t,t)})),{textConfig:i,textContent:r}}function Kk(t,e){e&&(e.font=e.textFont||e.font,dt(e,"textStrokeWidth")&&(t.lineWidth=e.textStrokeWidth),dt(e,"textAlign")&&(t.align=e.textAlign),dt(e,"textVerticalAlign")&&(t.verticalAlign=e.textVerticalAlign),dt(e,"textLineHeight")&&(t.lineHeight=e.textLineHeight),dt(e,"textWidth")&&(t.width=e.textWidth),dt(e,"textHeight")&&(t.height=e.textHeight),dt(e,"textBackgroundColor")&&(t.backgroundColor=e.textBackgroundColor),dt(e,"textPadding")&&(t.padding=e.textPadding),dt(e,"textBorderColor")&&(t.borderColor=e.textBorderColor),dt(e,"textBorderWidth")&&(t.borderWidth=e.textBorderWidth),dt(e,"textBorderRadius")&&(t.borderRadius=e.textBorderRadius),dt(e,"textBoxShadowColor")&&(t.shadowColor=e.textBoxShadowColor),dt(e,"textBoxShadowBlur")&&(t.shadowBlur=e.textBoxShadowBlur),dt(e,"textBoxShadowOffsetX")&&(t.shadowOffsetX=e.textBoxShadowOffsetX),dt(e,"textBoxShadowOffsetY")&&(t.shadowOffsetY=e.textBoxShadowOffsetY))}function $k(t,e,n){var i=t;i.textPosition=i.textPosition||n.position||"inside",null!=n.offset&&(i.textOffset=n.offset),null!=n.rotation&&(i.textRotation=n.rotation),null!=n.distance&&(i.textDistance=n.distance);var r=i.textPosition.indexOf("inside")>=0,o=t.fill||"#000";Jk(i,e);var a=null==i.textFill;return r?a&&(i.textFill=n.insideFill||"#fff",!i.textStroke&&n.insideStroke&&(i.textStroke=n.insideStroke),!i.textStroke&&(i.textStroke=o),null==i.textStrokeWidth&&(i.textStrokeWidth=2)):(a&&(i.textFill=t.fill||n.outsideFill||"#000"),!i.textStroke&&n.outsideStroke&&(i.textStroke=n.outsideStroke)),i.text=e.text,i.rich=e.rich,P(e.rich,(function(t){Jk(t,t)})),i}function Jk(t,e){e&&(dt(e,"fill")&&(t.textFill=e.fill),dt(e,"stroke")&&(t.textStroke=e.fill),dt(e,"lineWidth")&&(t.textStrokeWidth=e.lineWidth),dt(e,"font")&&(t.font=e.font),dt(e,"fontStyle")&&(t.fontStyle=e.fontStyle),dt(e,"fontWeight")&&(t.fontWeight=e.fontWeight),dt(e,"fontSize")&&(t.fontSize=e.fontSize),dt(e,"fontFamily")&&(t.fontFamily=e.fontFamily),dt(e,"align")&&(t.textAlign=e.align),dt(e,"verticalAlign")&&(t.textVerticalAlign=e.verticalAlign),dt(e,"lineHeight")&&(t.textLineHeight=e.lineHeight),dt(e,"width")&&(t.textWidth=e.width),dt(e,"height")&&(t.textHeight=e.height),dt(e,"backgroundColor")&&(t.textBackgroundColor=e.backgroundColor),dt(e,"padding")&&(t.textPadding=e.padding),dt(e,"borderColor")&&(t.textBorderColor=e.borderColor),dt(e,"borderWidth")&&(t.textBorderWidth=e.borderWidth),dt(e,"borderRadius")&&(t.textBorderRadius=e.borderRadius),dt(e,"shadowColor")&&(t.textBoxShadowColor=e.shadowColor),dt(e,"shadowBlur")&&(t.textBoxShadowBlur=e.shadowBlur),dt(e,"shadowOffsetX")&&(t.textBoxShadowOffsetX=e.shadowOffsetX),dt(e,"shadowOffsetY")&&(t.textBoxShadowOffsetY=e.shadowOffsetY),dt(e,"textShadowColor")&&(t.textShadowColor=e.textShadowColor),dt(e,"textShadowBlur")&&(t.textShadowBlur=e.textShadowBlur),dt(e,"textShadowOffsetX")&&(t.textShadowOffsetX=e.textShadowOffsetX),dt(e,"textShadowOffsetY")&&(t.textShadowOffsetY=e.textShadowOffsetY))}var Qk=La.CMD,tP=2*Math.PI,eP=["x","y"],nP=["width","height"],iP=[];function rP(t,e){return Math.abs(t-e)<1e-5}function oP(t){var e,n,i,r,o,a=t.data,s=t.len(),l=[],u=0,h=0,c=0,p=0;function d(t,n){e&&e.length>2&&l.push(e),e=[t,n]}function f(t,n,i,r){rP(t,i)&&rP(n,r)||e.push(t,n,i,r,i,r)}function g(t,n,i,r,o,a){var s=Math.abs(n-t),l=4*Math.tan(s/4)/3,u=n =0},e.prototype.getOrient=function(){return"vertical"===this.get("orient")?{index:1,name:"vertical"}:{index:0,name:"horizontal"}},e.type="legend.plain",e.dependencies=["series"],e.defaultOption={zlevel:0,z:4,show:!0,orient:"horizontal",left:"center",top:0,align:"auto",backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",borderRadius:0,borderWidth:0,padding:5,itemGap:10,itemWidth:25,itemHeight:14,symbolRotate:"inherit",inactiveColor:"#ccc",inactiveBorderColor:"#ccc",inactiveBorderWidth:"auto",itemStyle:{color:"inherit",opacity:"inherit",decal:"inherit",shadowBlur:0,shadowColor:null,shadowOffsetX:0,shadowOffsetY:0,borderColor:"inherit",borderWidth:"auto",borderCap:"inherit",borderJoin:"inherit",borderDashOffset:"inherit",borderMiterLimit:"inherit"},lineStyle:{width:"auto",color:"inherit",inactiveColor:"#ccc",inactiveWidth:2,opacity:"inherit",type:"inherit",cap:"inherit",join:"inherit",dashOffset:"inherit",miterLimit:"inherit",shadowBlur:0,shadowColor:null,shadowOffsetX:0,shadowOffsetY:0},textStyle:{color:"#333"},selectedMode:!0,selector:!1,selectorLabel:{show:!0,borderRadius:10,padding:[3,5,3,5],fontSize:12,fontFamily:" sans-serif",color:"#666",borderWidth:1,borderColor:"#666"},emphasis:{selectorLabel:{show:!0,color:"#eee",backgroundColor:"#666"}},selectorPosition:"auto",selectorItemGap:7,selectorButtonGap:10,tooltip:{show:!1}},e}(Xc),hV=B,cV=P,pV=Ei,dV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.newlineDisabled=!1,n}return n(e,t),e.prototype.init=function(){this.group.add(this._contentGroup=new pV),this.group.add(this._selectorGroup=new pV),this._isFirstRender=!0},e.prototype.getContentGroup=function(){return this._contentGroup},e.prototype.getSelectorGroup=function(){return this._selectorGroup},e.prototype.render=function(t,e,n){var i=this._isFirstRender;if(this._isFirstRender=!1,this.resetInner(),t.get("show",!0)){var r=t.get("align"),o=t.get("orient");r&&"auto"!==r||(r="right"===t.get("left")&&"vertical"===o?"right":"left");var a=t.get("selector",!0),s=t.get("selectorPosition",!0);!a||s&&"auto"!==s||(s="horizontal"===o?"end":"start"),this.renderInner(r,t,e,n,a,o,s);var l=t.getBoxLayoutParams(),u={width:n.getWidth(),height:n.getHeight()},h=t.get("padding"),c=Vc(l,u,h),p=this.layoutInner(t,r,c,i,a,s),d=Vc(T({width:p.width,height:p.height},l),u,h);this.group.x=d.x-p.x,this.group.y=d.y-p.y,this.group.markRedraw(),this.group.add(this._backgroundEl=BN(p,t))}},e.prototype.resetInner=function(){this.getContentGroup().removeAll(),this._backgroundEl&&this.group.remove(this._backgroundEl),this.getSelectorGroup().removeAll()},e.prototype.renderInner=function(t,e,n,i,r,o,a){var s=this.getContentGroup(),l=ht(),u=e.get("selectedMode"),h=[];n.eachRawSeries((function(t){!t.get("legendHoverLink")&&h.push(t.id)})),cV(e.getData(),(function(r,o){var a=r.get("name");if(!this.newlineDisabled&&(""===a||"\n"===a)){var c=new pV;return c.newline=!0,void s.add(c)}var p=n.getSeriesByName(a)[0];if(!l.get(a)){if(p){var d=p.getData(),f=d.getVisual("legendLineStyle")||{},g=d.getVisual("legendIcon"),y=d.getVisual("style");this._createItem(p,a,o,r,e,t,f,y,g,u).on("click",hV(fV,a,null,i,h)).on("mouseover",hV(yV,p.name,null,i,h)).on("mouseout",hV(vV,p.name,null,i,h)),l.set(a,!0)}else n.eachRawSeries((function(n){if(!l.get(a)&&n.legendVisualProvider){var s=n.legendVisualProvider;if(!s.containName(a))return;var c=s.indexOfName(a),p=s.getItemVisual(c,"style"),d=s.getItemVisual(c,"legendIcon"),f=He(p.fill);f&&0===f[3]&&(f[3]=.2,p.fill=Je(f,"rgba")),this._createItem(n,a,o,r,e,t,{},p,d,u).on("click",hV(fV,null,a,i,h)).on("mouseover",hV(yV,null,a,i,h)).on("mouseout",hV(vV,null,a,i,h)),l.set(a,!0)}}),this);0}}),this),r&&this._createSelector(r,e,i,o,a)},e.prototype._createSelector=function(t,e,n,i,r){var o=this.getSelectorGroup();cV(t,(function(t){var i=t.type,r=new cs({style:{x:0,y:0,align:"center",verticalAlign:"middle"},onclick:function(){n.dispatchAction({type:"all"===i?"legendAllSelect":"legendInverseSelect"})}});o.add(r),hh(r,{normal:e.getModel("selectorLabel"),emphasis:e.getModel(["emphasis","selectorLabel"])},{defaultText:t.title}),sl(r)}))},e.prototype._createItem=function(t,e,n,i,r,o,a,s,l,u){var h=t.visualDrawType,c=r.get("itemWidth"),p=r.get("itemHeight"),d=r.isSelected(e),f=i.get("symbolRotate"),g=i.get("icon"),y=function(t,e,n,i,r,o,a){for(var s=e.getModel("itemStyle"),l=Lh.concat([["decal"]]),u={},h=0;h5)return;var i=this._model.coordinateSystem.getSlidedAxisExpandWindow([t.offsetX,t.offsetY]);"none"!==i.behavior&&this._dispatchExpand({axisExpandWindow:i.axisExpandWindow})}this._mouseDownPoint=null},mousemove:function(t){if(!this._mouseDownPoint&&AD(this,"mousemove")){var e=this._model,n=e.coordinateSystem.getSlidedAxisExpandWindow([t.offsetX,t.offsetY]),i=n.behavior;"jump"===i&&this._throttledDispatchExpand.debounceNextCall(e.get("axisExpandDebounce")),this._throttledDispatchExpand("none"===i?null:{axisExpandWindow:n.axisExpandWindow,animation:"jump"===i?null:{duration:0}})}}};function AD(t,e){var n=t._model;return n.get("axisExpandable")&&n.get("axisExpandTriggerOn")===e}var LD=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(){t.prototype.init.apply(this,arguments),this.mergeOption({})},e.prototype.mergeOption=function(t){var e=this.option;t&&S(e,t,!0),this._initDimensions()},e.prototype.contains=function(t,e){var n=t.get("parallelIndex");return null!=n&&e.getComponent("parallel",n)===this},e.prototype.setAxisExpand=function(t){P(["axisExpandable","axisExpandCenter","axisExpandCount","axisExpandWidth","axisExpandWindow"],(function(e){t.hasOwnProperty(e)&&(this.option[e]=t[e])}),this)},e.prototype._initDimensions=function(){var t=this.dimensions=[],e=this.parallelAxisIndex=[];P(N(this.ecModel.queryComponents({mainType:"parallelAxis"}),(function(t){return(t.get("parallelIndex")||0)===this.componentIndex}),this),(function(n){t.push("dim"+n.get("dim")),e.push(n.componentIndex)}))},e.type="parallel",e.dependencies=["parallelAxis"],e.layoutMode="box",e.defaultOption={zlevel:0,z:0,left:80,top:60,right:80,bottom:60,layout:"horizontal",axisExpandable:!1,axisExpandCenter:null,axisExpandCount:0,axisExpandWidth:50,axisExpandRate:17,axisExpandDebounce:50,axisExpandSlideTriggerArea:[-.15,.05,.4],axisExpandTriggerOn:"click",parallelAxisDefault:null},e}(Xc),kD=function(t){function e(e,n,i,r,o){var a=t.call(this,e,n,i)||this;return a.type=r||"value",a.axisIndex=o,a}return n(e,t),e.prototype.isHorizontal=function(){return"horizontal"!==this.coordinateSystem.getModel().get("layout")},e}(hb);function PD(t,e,n,i,r,o){t=t||0;var a=n[1]-n[0];if(null!=r&&(r=RD(r,[0,a])),null!=o&&(o=Math.max(o,null!=r?r:0)),"all"===i){var s=Math.abs(e[1]-e[0]);s=RD(s,[0,a]),r=o=RD(s,[r,o]),i=0}e[0]=RD(e[0],n),e[1]=RD(e[1],n);var l=OD(e,i);e[i]+=t;var u,h=r||0,c=n.slice();return l.sign<0?c[0]+=h:c[1]-=h,e[i]=RD(e[i],c),u=OD(e,i),null!=r&&(u.sign!==l.sign||u.span',x=window.open();x.document.write(_),x.document.title=i}else{var b=document.createElement("a");b.download=i+"."+o,b.target="_blank",b.href=s;var w=new MouseEvent("click",{view:document.defaultView,bubbles:!0,cancelable:!1});b.dispatchEvent(w)}},e.getDefaultOption=function(t){return{show:!0,icon:"M4.7,22.9L29.3,45.5L54.7,23.4M4.6,43.6L4.6,58L53.8,58L53.8,43.6M29.2,45.1L29.2,0",title:t.getLocale(["toolbox","saveAsImage","title"]),type:"png",connectedBackgroundColor:"#fff",name:"",excludeComponents:["toolbox"],lang:t.getLocale(["toolbox","saveAsImage","lang"])}},e}(RN);GN.prototype.unusable=!a.canvasSupported;var HN="__ec_magicType_stack__",WN=[["line","bar"],["stack"]],UN=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.getIcons=function(){var t=this.model,e=t.get("icon"),n={};return P(t.get("type"),(function(t){e[t]&&(n[t]=e[t])})),n},e.getDefaultOption=function(t){return{show:!0,type:[],icon:{line:"M4.1,28.9h7.1l9.3-22l7.4,38l9.7-19.7l3,12.8h14.9M4.1,58h51.4",bar:"M6.7,22.9h10V48h-10V22.9zM24.9,13h10v35h-10V13zM43.2,2h10v46h-10V2zM3.1,58h53.7",stack:"M8.2,38.4l-8.4,4.1l30.6,15.3L60,42.5l-8.1-4.1l-21.5,11L8.2,38.4z M51.9,30l-8.1,4.2l-13.4,6.9l-13.9-6.9L8.2,30l-8.4,4.2l8.4,4.2l22.2,11l21.5-11l8.1-4.2L51.9,30z M51.9,21.7l-8.1,4.2L35.7,30l-5.3,2.8L24.9,30l-8.4-4.1l-8.3-4.2l-8.4,4.2L8.2,30l8.3,4.2l13.9,6.9l13.4-6.9l8.1-4.2l8.1-4.1L51.9,21.7zM30.4,2.2L-0.2,17.5l8.4,4.1l8.3,4.2l8.4,4.2l5.5,2.7l5.3-2.7l8.1-4.2l8.1-4.2l8.1-4.1L30.4,2.2z"},title:t.getLocale(["toolbox","magicType","title"]),option:{},seriesIndex:{}}},e.prototype.onclick=function(t,e,n){var i=this.model,r=i.get(["seriesIndex",n]);if(XN[n]){var o,a={series:[]};P(WN,(function(t){D(t,n)>=0&&P(t,(function(t){i.setIconStatus(t,"normal")}))})),i.setIconStatus(n,"emphasis"),t.eachComponent({mainType:"series",query:null==r?null:{seriesIndex:r}},(function(t){var e=t.subType,r=t.id,o=XN[n](e,r,t,i);o&&(T(o,t.option),a.series.push(o));var s=t.coordinateSystem;if(s&&"cartesian2d"===s.type&&("line"===n||"bar"===n)){var l=s.getAxesByScale("ordinal")[0];if(l){var u=l.dim+"Axis",h=t.getReferringComponents(u,Nr).models[0].componentIndex;a[u]=a[u]||[];for(var c=0;c<=h;c++)a[u][h]=a[u][h]||{};a[u][h].boundaryGap="bar"===n}}}));var s=n;"stack"===n&&(o=S({stack:i.option.title.tiled,tiled:i.option.title.stack},i.option.title),"emphasis"!==i.get(["iconStatus",n])&&(s="tiled")),e.dispatchAction({type:"changeMagicType",currentType:s,newOption:a,newTitle:o,featureName:"magicType"})}},e}(RN),XN={line:function(t,e,n,i){if("bar"===t)return S({id:e,type:"line",data:n.get("data"),stack:n.get("stack"),markPoint:n.get("markPoint"),markLine:n.get("markLine")},i.get(["option","line"])||{},!0)},bar:function(t,e,n,i){if("line"===t)return S({id:e,type:"bar",data:n.get("data"),stack:n.get("stack"),markPoint:n.get("markPoint"),markLine:n.get("markLine")},i.get(["option","bar"])||{},!0)},stack:function(t,e,n,i){var r=n.get("stack")===HN;if("line"===t||"bar"===t)return i.setIconStatus("stack",r?"normal":"emphasis"),S({id:e,stack:r?"":HN},i.get(["option","stack"])||{},!0)}};Hm({type:"changeMagicType",event:"magicTypeChanged",update:"prepareAndUpdate"},(function(t,e){e.mergeOption(t.newOption)}));var YN=new Array(60).join("-"),ZN="\t";function jN(t){return t.replace(/^\s\s*/,"").replace(/\s\s*$/,"")}var qN=new RegExp("[\t]+","g");function KN(t,e){var n=t.split(new RegExp("\n*"+YN+"\n*","g")),i={series:[]};return P(n,(function(t,n){if(function(t){if(t.slice(0,t.indexOf("\n")).indexOf(ZN)>=0)return!0}(t)){var r=function(t){for(var e=t.split(/\n+/g),n=[],i=O(jN(e.shift()).split(qN),(function(t){return{name:t,data:[]}})),r=0;r
",g=u.join(f);this._showOrMove(o,(function(){this._updateContentNotChangedOnAxis(t)?this._updatePosition(o,c,r[0],r[1],this._tooltipContent,s):this._showTooltipContent(o,g,s,Math.random()+"",r[0],r[1],c,null,h)}))},e.prototype._showSeriesItemTooltip=function(t,e,n){var i=this._ecModel,r=_s(e),o=r.seriesIndex,a=i.getSeriesByIndex(o),s=r.dataModel||a,l=r.dataIndex,u=r.dataType,h=s.getData(u),c=this._renderMode,p=t.positionDefault,d=Uz([h.getItemModel(l),s,a&&(a.coordinateSystem||{}).model],this._tooltipModel,p?{position:p}:null),f=d.get("trigger");if(null==f||"item"===f){var g=s.getDataParams(l,u),y=new hf;g.marker=y.makeTooltipMarker("item",kc(g.color),c);var v=Cd(s.formatTooltip(l,!1,u)),m=d.get("order"),_=v.markupFragment?rf(v.markupFragment,y,c,m,i.get("useUTC"),d.get("textStyle")):v.markupText,x="item_"+s.name+"_"+l;this._showOrMove(d,(function(){this._showTooltipContent(d,_,g,x,t.offsetX,t.offsetY,t.position,t.target,y)})),n({type:"showTip",dataIndexInside:l,dataIndex:h.getRawIndex(l),seriesIndex:o,from:this.uid})}},e.prototype._showComponentItemTooltip=function(t,e,n){var i=_s(e),r=i.tooltipConfig.option||{};if(H(r)){r={content:r,formatter:r}}var o=[r],a=this._ecModel.getComponent(i.componentMainType,i.componentIndex);a&&o.push(a),o.push({formatter:r.content});var s=t.positionDefault,l=Uz(o,this._tooltipModel,s?{position:s}:null),u=l.get("content"),h=Math.random()+"",c=new hf;this._showOrMove(l,(function(){var n=w(l.get("formatterParams")||{});this._showTooltipContent(l,u,n,h,t.offsetX,t.offsetY,t.position,e,c)})),n({type:"showTip",from:this.uid})},e.prototype._showTooltipContent=function(t,e,n,i,r,o,a,s,l){if(this._ticket="",t.get("showContent")&&t.get("show")){var u=this._tooltipContent,h=t.get("formatter");a=a||t.get("position");var c=e,p=this._getNearestPoint([r,o],n,t.get("trigger"),t.get("borderColor")).color;if(h&&H(h)){var d=t.ecModel.get("useUTC"),f=F(n)?n[0]:n;c=h,f&&f.axisType&&f.axisType.indexOf("time")>=0&&(c=ic(f.axisValue,c,d)),c=Ac(c,n,!0)}else if(G(h)){var g=Bz((function(e,i){e===this._ticket&&(u.setContent(i,l,t,p,a),this._updatePosition(t,a,r,o,u,n,s))}),this);this._ticket=i,c=h(n,i,g)}u.setContent(c,l,t,p,a),u.show(t,p),this._updatePosition(t,a,r,o,u,n,s)}},e.prototype._getNearestPoint=function(t,e,n,i){return"axis"===n||F(e)?{color:i||("html"===this._renderMode?"#fff":"none")}:F(e)?void 0:{color:i||e.color||e.borderColor}},e.prototype._updatePosition=function(t,e,n,i,r,o,a){var s=this._api.getWidth(),l=this._api.getHeight();e=e||t.get("position");var u=r.getSize(),h=t.get("align"),c=t.get("verticalAlign"),p=a&&a.getBoundingRect().clone();if(a&&p.applyTransform(a.transform),G(e)&&(e=e([n,i],o,r.el,p,{viewSize:[s,l],contentSize:u.slice()})),F(e))n=Gz(e[0],s),i=Gz(e[1],l);else if(X(e)){var d=e;d.width=u[0],d.height=u[1];var f=Vc(d,{width:s,height:l});n=f.x,i=f.y,h=null,c=null}else if(H(e)&&a){var g=function(t,e,n){var i=n[0],r=n[1],o=10,a=5,s=0,l=0,u=e.width,h=e.height;switch(t){case"inside":s=e.x+u/2-i/2,l=e.y+h/2-r/2;break;case"top":s=e.x+u/2-i/2,l=e.y-r-o;break;case"bottom":s=e.x+u/2-i/2,l=e.y+h+o;break;case"left":s=e.x-i-o-a,l=e.y+h/2-r/2;break;case"right":s=e.x+u+o+a,l=e.y+h/2-r/2}return[s,l]}(e,p,u);n=g[0],i=g[1]}else{g=function(t,e,n,i,r,o,a){var s=n.getOuterSize(),l=s.width,u=s.height;null!=o&&(t+l+o+2>i?t-=l+o:t+=o);null!=a&&(e+u+a>r?e-=u+a:e+=a);return[t,e]}(n,i,r,s,l,h?null:20,c?null:20);n=g[0],i=g[1]}if(h&&(n-=Yz(h)?u[0]/2:"right"===h?u[0]:0),c&&(i-=Yz(c)?u[1]/2:"bottom"===c?u[1]:0),Sz(t)){g=function(t,e,n,i,r){var o=n.getOuterSize(),a=o.width,s=o.height;return t=Math.min(t+a,i)-a,e=Math.min(e+s,r)-s,t=Math.max(t,0),e=Math.max(e,0),[t,e]}(n,i,r,s,l);n=g[0],i=g[1]}r.moveTo(n,i)},e.prototype._updateContentNotChangedOnAxis=function(t){var e=this._lastDataByCoordSys,n=!!e&&e.length===t.length;return n&&Fz(e,(function(e,i){var r=e.dataByAxis||[],o=(t[i]||{}).dataByAxis||[];(n=n&&r.length===o.length)&&Fz(r,(function(t,e){var i=o[e]||{},r=t.seriesDataIndices||[],a=i.seriesDataIndices||[];(n=n&&t.value===i.value&&t.axisType===i.axisType&&t.axisId===i.axisId&&r.length===a.length)&&Fz(r,(function(t,e){var i=a[e];n=n&&t.seriesIndex===i.seriesIndex&&t.dataIndex===i.dataIndex}))}))})),this._lastDataByCoordSys=t,!!n},e.prototype._hide=function(t){this._lastDataByCoordSys=null,t({type:"hideTip",from:this.uid})},e.prototype.dispose=function(t,e){a.node||(this._tooltipContent.dispose(),KO("itemTooltip",e))},e.type="tooltip",e}(wf);function Uz(t,e,n){var i,r=e.ecModel;n?(i=new Oh(n,r,r),i=new Oh(e.option,i,r)):i=e;for(var o=t.length-1;o>=0;o--){var a=t[o];a&&(a instanceof Oh&&(a=a.get("tooltip",!0)),H(a)&&(a={formatter:a}),a&&(i=new Oh(a,i,r)))}return i}function Xz(t,e){return t.dispatchAction||V(e.dispatchAction,e)}function Yz(t){return"center"===t||"middle"===t}var Zz=["rect","polygon","keep","clear"];function jz(t,e){var n=xr(t?t.brush:[]);if(n.length){var i=[];P(n,(function(t){var e=t.hasOwnProperty("toolbox")?t.toolbox:[];e instanceof Array&&(i=i.concat(e))}));var r=t&&t.toolbox;F(r)&&(r=r[0]),r||(r={feature:{}},t.toolbox=[r]);var o=r.feature||(r.feature={}),a=o.brush||(o.brush={}),s=a.type||(a.type=[]);s.push.apply(s,i),function(t){var e={};P(t,(function(t){e[t]=1})),t.length=0,P(e,(function(e,n){t.push(n)}))}(s),e&&!s.length&&s.push.apply(s,Zz)}}var qz=P;function Kz(t){if(t)for(var e in t)if(t.hasOwnProperty(e))return!0}function $z(t,e,n){var i={};return qz(e,(function(e){var r,o=i[e]=((r=function(){}).prototype.__hidden=r.prototype,new r);qz(t[e],(function(t,i){if(TT.isValidType(i)){var r={type:i,visual:t};n&&n(r,e),o[i]=new TT(r),"opacity"===i&&((r=w(r)).type="colorAlpha",o.__hidden.__alphaForOpacity=new TT(r))}}))})),i}function Jz(t,e,n){var i;P(n,(function(t){e.hasOwnProperty(t)&&Kz(e[t])&&(i=!0)})),i&&P(n,(function(n){e.hasOwnProperty(n)&&Kz(e[n])?t[n]=w(e[n]):delete t[n]}))}var Qz={lineX:tE(0),lineY:tE(1),rect:{point:function(t,e,n){return t&&n.boundingRect.contain(t[0],t[1])},rect:function(t,e,n){return t&&n.boundingRect.intersect(t)}},polygon:{point:function(t,e,n){return t&&n.boundingRect.contain(t[0],t[1])&&uv(n.range,t[0],t[1])},rect:function(t,e,n){var i=n.range;if(!t||i.length<=1)return!1;var r=t.x,o=t.y,a=t.width,s=t.height,l=i[0];return!!(uv(i,r,o)||uv(i,r+a,o)||uv(i,r,o+s)||uv(i,r+a,o+s)||gi.create(t).contain(l[0],l[1])||nh(r,o,r+a,o,i)||nh(r,o,r,o+s,i)||nh(r+a,o,r+a,o+s,i)||nh(r,o+s,r+a,o+s,i))||void 0}}};function tE(t){var e=["x","y"],n=["width","height"];return{point:function(e,n,i){if(e){var r=i.range;return eE(e[t],r)}},rect:function(i,r,o){if(i){var a=o.range,s=[i[e[t]],i[e[t]]+i[n[t]]];return s[1]e[0][1]&&(e[0][1]=o[0]),o[1]