2023-08-12
Node.js
00

目录

简单唠唠
梳理思路
运动数据总和
最新动态
express创建接口
最后的话

简单唠唠

某乎问题:人这一生,应该养成哪些好习惯?

问题链接:https://www.zhihu.com/question/460674063

如果我来回答肯定会有定期运动的字眼。

平日里也有煅练的习惯,时间久了后一直想把运动数据公开,可惜某运动软件未开放公共的接口出来。

幸运的是,在Github平台冲浪我发现了有同行和我有类似的想法,并且已经用Python实现了他自己的运动主页。

项目链接:https://github.com/yihong0618/running_page

Python嘛简单,看明白后用Node.js折腾一波,自己撸两个接口玩玩。

完成的运动页面挂在我的博客网址。

我的博客:https://www.linglan01.cn

我的运动主页:https://www.linglan01.cn/c/keep/index.html

Github地址:https://github.com/CatsAndMice/keep

梳理思路

平时跑步、骑行这两项活动多,所以我只需要调用这两个接口,再调用这两个接口前需要先登录获取到token。

Bash
1. 登陆接口: https://api.gotokeep.com/v1.1/users/login 请求方法:post Content-Type: "application/x-www-form-urlencoded;charset=utf-8" 2. 骑行数据接口:https://api.gotokeep.com/pd/v3/stats/detail?dateUnit=all&type=cycling&lastDate={last_date} 请求方法: get Content-Type: "application/x-www-form-urlencoded;charset=utf-8" Authorization:`Bearer ${token}` 3. 跑步数据接口:https://api.gotokeep.com/pd/v3/stats/detail?dateUnit=all&type=running&lastDate={last_date} 请求方法: get Content-Type: "application/x-www-form-urlencoded;charset=utf-8" Authorization:`Bearer ${token}`

Node.js服务属于代理层,解决跨域问题并再对数据包裹一层逻辑处理,最后发给客户端。

不明白代理的同学可以看看这篇《Nginx之正、反向代理》

文章链接:https://linglan01.cn/post/47

运动数据总和

请求跑步接口方法:

getRunning.js文件链接https://github.com/CatsAndMice/keep/blob/master/src/getRunning.js

JavaScript
const { headers } = require('./config'); const { isEmpty } = require("medash"); const axios = require('axios'); module.exports = async (token, last_date = 0) => { if (isEmpty(token)) return {} headers["Authorization"] = `Bearer ${token}`; const result = await axios.get(`https://api.gotokeep.com/pd/v3/stats/detail?dateUnit=all&type=running&lastDate=${last_date}`, { headers }) if (result.status === 200) { const { data: loginResult } = result; return loginResult.data; } return {}; }

请求骑行接口方法:

getRunning.js文件链接 https://github.com/CatsAndMice/keep/blob/master/src/getCycling.js

JavaScript
const { headers } = require('./config'); const { isEmpty } = require("medash"); const axios = require('axios'); module.exports = async (token, last_date = 0) => { if (isEmpty(token)) return {} headers["Authorization"] = `Bearer ${token}`; const result = await axios.get(`https://api.gotokeep.com/pd/v3/stats/detail?dateUnit=all&type=cycling&lastDate=${last_date}`, { headers }) if (result.status === 200) { const { data: loginResult } = result; return loginResult.data; } return {}; }

现在要计算跑步、骑行的总数据,因此需要分别请求跑步、骑行的接口获取到所有的数据。

getAllLogs.js文件链接https://github.com/CatsAndMice/keep/blob/master/src/getAllLogs.js

JavaScript
const { isEmpty } = require('medash'); module.exports = async (token, firstResult, callback) => { if (isEmpty(firstResult)||isEmpty(token)) { console.warn('请求中断'); return; } let { lastTimestamp, records = [] } = firstResult; while (1) { if (isEmpty(lastTimestamp)) break; const result = await callback(token, lastTimestamp) if (isEmpty(result)) break; const { lastTimestamp: lastTime, records: nextRecords } = result records.push(...nextRecords); if (isEmpty(lastTime)) break; lastTimestamp = lastTime } return records }

一个while循环干到底,所有的数据都会被pushrecords数组中。

返回的records数据再按年份分类计算某年的总骑行数或总跑步数,使用Map做这类事别提多爽了。

getYearTotal.js文件链接 https://github.com/CatsAndMice/keep/blob/master/src/getYearTotal.js

JavaScript
const { getYmdHms, mapToObj, each, isEmpty } = require('medash'); module.exports = (totals = []) => { const yearMap = new Map() totals.forEach((t) => { const { logs = [] } = t logs.forEach(log => { if(isEmpty(log))return const { stats: { endTime, kmDistance } } = log const { year } = getYmdHms(endTime); const mapValue = yearMap.get(year); if (mapValue) { yearMap.set(year, mapValue + kmDistance); return } yearMap.set(year, kmDistance); }) }) let keepRunningTotals = []; each(mapToObj(yearMap), (key, value) => { keepRunningTotals.push({ year: key, kmDistance: Math.ceil(value) }); }) return keepRunningTotals.sort((a, b) => { return b.year - a.year; }); }

处理过后的数据是这样子的:

JSON
[ {year:2023,kmDistance:99}, {year:2022,kmDistance:66}, //... ]

计算跑步、骑行的逻辑,唯一的变量为请求接口方法的不同,getAllLogs.js、getYearTotal.js我们可以复用。

骑行计算总和:

cycling.js文件链接https://github.com/CatsAndMice/keep/blob/master/src/cycling.js

JavaScript
const getCycling = require('./getCycling'); const getAllLogs = require('./getAllLogs'); const getYearTotal = require('./getYearTotal'); module.exports = async (token) => { const result = await getCycling(token) const allCycling = await getAllLogs(token, result, getCycling); const yearCycling = getYearTotal(allCycling) return yearCycling }

跑步计算总和:

run.js文件链接 https://github.com/CatsAndMice/keep/blob/master/src/run.js

JavaScript
const getRunning = require('./getRunning'); const getAllRunning = require('./getAllLogs'); const getYearTotal = require('./getYearTotal'); module.exports = async (token) => { const result = await getRunning(token) // 获取全部的跑步数据 const allRunning = await getAllRunning(token, result, getRunning); // 按年份计算跑步运动量 const yearRunning = getYearTotal(allRunning) return yearRunning }

最后一步,骑行、跑步同年份数据汇总。

src/index.js文件链接https://github.com/CatsAndMice/keep/blob/master/src/index.js

JavaScript
const login = require('./login'); const getRunTotal = require('./run'); const getCycleTotal = require('./cycling'); const { isEmpty, toArray } = require("medash"); require('dotenv').config(); const query = { token: '', date: 0 } const two = 2 * 24 * 60 * 60 * 1000 const data = { mobile: process.env.MOBILE, password: process.env.PASSWORD }; const getTotal = async () => { const diff = Math.abs(Date.now() - query.date); if (diff > two) { const token = await login(data); query.token = token; query.date = Date.now(); } //Promise.all并行请求 const result = await Promise.all([getRunTotal(query.token), getCycleTotal(query.token)]) const yearMap = new Map(); if (isEmpty(result)) return; if (isEmpty(result[0])) return; result[0].forEach(r => { const { year, kmDistance } = r; const mapValue = yearMap.get(year); if (mapValue) { mapValue.year = year mapValue.data.runKmDistance = kmDistance } else { yearMap.set(year, { year, data: { runKmDistance: kmDistance, cycleKmDistance: 0 } }) } }) if (isEmpty(result[1])) return; result[1].forEach(r => { const { year, kmDistance } = r; const mapValue = yearMap.get(year); if (mapValue) { mapValue.year = year mapValue.data.cycleKmDistance = kmDistance } else { yearMap.set(year, { year, data: { runKmDistance: 0, cycleKmDistance: kmDistance } }) } }) return toArray(yearMap.values()) } module.exports = { getTotal }

getTotal方法会将跑步、骑行数据汇总成这样:

JSON
[ { year:2023, runKmDistance: 999,//2023年,跑步总数据 cycleKmDistance: 666//2023年,骑行总数据 }, { year:2022, runKmDistance: 99, cycleKmDistance: 66 }, //... ]

每次调用getTotal方法都会调用login方法获取一次token。这里做了一个优化,获取的token会被缓存2天省得每次都调,调多了登陆接口会出问题。

JavaScript
//省略 const query = { token: '', date: 0 } const two = 2 * 24 * 60 * 60 * 1000 const data = { mobile: process.env.MOBILE, password: process.env.PASSWORD }; const getTotal = async () => { const diff = Math.abs(Date.now() - query.date); if (diff > two) { const token = await login(data); query.token = token; query.date = Date.now(); } //省略 } //省略

最新动态

骑行、跑步接口都只请求一次,同年同月同日的骑行、跑步数据放在一起,最后按endTime字段的时间倒序返回结果。

getRecentUpdates.js文件链接 https://github.com/CatsAndMice/keep/blob/master/src/getRecentUpdates.js

JavaScript
const getRunning = require('./getRunning'); const getCycling = require('./getCycling'); const { isEmpty, getYmdHms, toArray } = require('medash'); module.exports = async (token) => { if (isEmpty(token)) return const recentUpdateMap = new Map(); const result = await Promise.all([getRunning(token), getCycling(token)]); result.forEach((r) => { if (isEmpty(r)) return; const records = r.records || []; if (isEmpty(r.records)) return; records.forEach(rs => { rs.logs.forEach(l => { const { stats } = l; if (isEmpty(stats)) return; // 运动距离小于1km 则忽略该动态 if (stats.kmDistance < 1) return; const { year, month, date, } = getYmdHms(stats.endTime); const key = `${year}${month + 1}${date}日`; const mapValue = recentUpdateMap.get(key); const value = `${stats.name} ${stats.kmDistance}km`; if (mapValue) { mapValue.data.push(value) } else { recentUpdateMap.set(key, { date: key, endTime: stats.endTime, data: [ value ] }); } }) }) }) return toArray(recentUpdateMap.values()).sort((a, b) => { return b.endTime - a.endTime }) }

得到的数据是这样的:

JSON
[ { date: '2023812', endTime: 1691834351501, data: [ '户外跑步 99km', '户外骑行 99km' ] }, //... ]

同样的要先获取token,在src/index.js文件:

JavaScript
const login = require('./login'); const getRecentUpdates = require('./getRecentUpdates'); //省略 const getFirstPageRecentUpdates = async () => { const diff = Math.abs(Date.now() - query.date); if (diff > two) { const token = await login(data); query.token = token; query.date = Date.now(); } return await getRecentUpdates(query.token); } //省略

最新动态这个接口还是简单的。

express创建接口

运动主页由于我要将其挂到我的博客,因为端口不同会出现跨域问题,所以要开启跨源资源共享(CORS)。

JavaScript
app.use((req, res, next) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); res.setHeader('Content-Type', 'application/json;charset=utf8'); next(); })

另外,我的博客网址使用的是https协议,Node.js服务也需要升级为https,否则会请求出错。以前写过一篇文章介绍Node.js升级https协议,不清楚的同学可以看看这篇《Node.js搭建Https服务 》文章链接https://linglan01.cn/post/47

index.js文件链接https://github.com/CatsAndMice/keep/blob/master/index.js

JavaScript
const express = require('express'); const { getTotal, getFirstPageRecentUpdates } = require("./src") const { to } = require('await-to-js'); const fs = require('fs'); const https = require('https'); const path = require('path'); const app = express(); const port = 3000; app.use((req, res, next) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); res.setHeader('Content-Type', 'application/json;charset=utf8'); next(); }) app.get('/total', async (req, res) => { const [err, result] = await to(getTotal()) if (result) { res.send(JSON.stringify({ code: 200, data: result, msg: '请求成功' })); return } res.send(JSON.stringify({ code: 400, data: null, msg: '请求失败' })); }) app.get('/recent-updates', async (req, res) => { const [err, result] = await to(getFirstPageRecentUpdates()) if (result) { res.send(JSON.stringify({ code: 200, data: result, msg: '请求成功' })); return } res.send(JSON.stringify({ code: 400, data: null, msg: '请求失败' })); }) const options = { key: fs.readFileSync(path.join(__dirname, './ssl/9499016_www.linglan01.cn.key')), cert: fs.readFileSync(path.join(__dirname, './ssl/9499016_www.linglan01.cn.pem')), }; const server = https.createServer(options, app); server.listen(port, () => { console.log('服务已开启'); })

最后的话

贵在坚持,做好「简单而正确」的事情,坚持是一项稀缺的能力,不仅仅是运动、写文章,在其他领域,也是如此。

这段时间对投资、理财小有研究,坚持运动也是一种对身体健康的投资。

又完成了一篇文章,奖励自己一顿火锅。 如果我的文章对你有帮助,您的👍就是对我的最大支持^_^。

更多文章:http://linglan01.cn/about

本文作者:凌览

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!