用Node.js吭哧吭哧撸一个运动主页

简单唠唠

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

问题链接: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。

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

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

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

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 } = resultrecords.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

const { getYmdHms, mapToObj, each, isEmpty } = require('medash');
module.exports = (totals = []) => {const yearMap = new Map()totals.forEach((t) => {const { logs = [] } = tlogs.forEach(log => {if(isEmpty(log))returnconst { stats: { endTime, kmDistance } } = logconst { 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;});
}

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

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

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

骑行计算总和:

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

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

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

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 = yearmapValue.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 = yearmapValue.data.cycleKmDistance = kmDistance} else {yearMap.set(year, {year, data: {runKmDistance: 0,cycleKmDistance: kmDistance}})}})return toArray(yearMap.values())
}
module.exports = {getTotal
}

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

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

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

//省略
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

const getRunning = require('./getRunning');
const getCycling = require('./getCycling');
const { isEmpty, getYmdHms, toArray } = require('medash');
module.exports = async (token) => {if (isEmpty(token)) returnconst 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})
}

得到的数据是这样的:

[{date: '2023年8月12',endTime: 1691834351501,data: ['户外跑步 99km','户外骑行 99km']},//...
]

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

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)。

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/37843.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

火山引擎DataLeap的Data Catalog系统公有云实践

更多技术交流、求职机会&#xff0c;欢迎关注字节跳动数据平台微信公众号&#xff0c;回复【1】进入官方交流群 Data Catalog是一种元数据管理的服务&#xff0c;会收集技术元数据&#xff0c;并在其基础上提供更丰富的业务上下文与语义&#xff0c;通常支持元数据编目、查找、…

自然数的拆分问题

题目描述 任何一个大于 11 的自然数 n&#xff0c;总可以拆分成若干个小于 n 的自然数之和。现在给你一个自n&#xff0c;要求你求出 n 的拆分成一些数字的和。每个拆分后的序列中的数字从小到大排序。然后你需要输出这些序列&#xff0c;其中字典序小的序列需要优先输出。 输…

搭建openGauss 5.0 一主一从复制集群

openGauss是一款支持SQL2003标准语法&#xff0c;支持主备部署的高可用关系型国产数据库。 多种存储模式支持复合业务场景&#xff0c;新引入提供原地更新存储引擎。NUMA化数据结构支持高性能。Paxos一致性日志复制协议&#xff0c;主备模式&#xff0c;CRC校验支持高可用。支…

设置返回列表元素上限

我正在「拾陆楼」和朋友们讨论有趣的话题&#xff0c;你⼀起来吧&#xff1f;拾陆楼知识星球入口 在get_cell &#xff0c;get_nets&#xff0c;get_xx等操作时返回的值上限是100&#xff0c;后面的就用...省略了&#xff0c;如果要修改这个上限&#xff0c;需要用下面命令: s…

设计模式之七大原则

&#x1f451;单一职责原则 单一职责原则告诉我们一个类应该只有一个责任或者只负责一件事情。 想象一下&#xff0c;如果一个类承担了太多的责任&#xff0c;就像一个人同时负责做饭、洗衣服和打扫卫生一样&#xff0c;那么这个类会变得非常复杂&#xff0c;难以理解和维护。而…

一些Git Repo

文章目录 Fake-TcpWow Fishing Script模拟券商柜台 Fake-Tcp Fake-Tcp 自己写的一个伪装包测试。 尝试把UDP的包伪装成TCP包&#xff0c;再发送到Internet Wow Fishing Script 魔兽世界钓鱼脚本 自己写的魔兽世界钓鱼脚本&#xff0c;10.0初期钓鱼成功率90%以上。现在关服了…

基于Spring Boot的高校图书馆管理系统的设计与实现(Java+spring boot+MySQL)

获取源码或者论文请私信博主 演示视频&#xff1a; 基于Spring Boot的高校图书馆管理系统的设计与实现&#xff08;Javaspring bootMySQL&#xff09; 使用技术&#xff1a; 前端&#xff1a;html css javascript jQuery ajax thymeleaf 微信小程序 后端&#xff1a;Java sp…

关于ChatGPT抽样调查:78%的人用于搜索,30%的人担心因它失业

人工智能早已不再被视为未来科技&#xff0c;而是越来越多地应用在时下人们的生活之中。根据DECO PROTESTE的调查&#xff0c;大约72%的葡萄牙人认为人工智能已经活跃于他们的日常。[1] 随着ChatGPT对各个行业的影响&#xff0c;也引发了人们关于这种人工智能模型潜力的争论&a…

c++模板的原理与使用

C中实现代码复用有两个方式&#xff1a;类的继承&#xff08;即实现了多态&#xff09;&#xff0c;以及模板的使用。这里介绍的模板的知识。 模板的目的&#xff1a; 同样的代码适用于不同类型下的使用&#xff0c;实现代码的复用目的。 模板的原理&#xff1a; 编译阶段&am…

Cygwin 配置C/C++编译环境以及如何编译项目

文章目录 一、安装C、C编译环境需要的包1. 选择gcc-core、gcc-g2. 选择gdb3. 选择mingw64下的gcc-core、gcc-g4. 选择make5. 选择cmake6. 确认更改7. 查看包安装状态 二、C、C 项目编译示例step1&#xff1a;解压缩sed-4.9.tar.gzstep2&#xff1a;执行./configure生成Makefile…

shell之正则表达式及三剑客grep命令

一、正则表达式概述 什么是正则表达式&#xff1f; 正则表达式是一种描述字符串匹配规则的重要工具 1、正则表达式定义: 正则表达式&#xff0c;又称正规表达式、常规表达式 使用字符串描述、匹配一系列符合某个规则的字符串 正则表达式 普通字符&#xff1a; 大小写字母…

opencv视频截取每一帧并保存为图片python代码CV2实现练习

当涉及到视频处理时&#xff0c;Python中的OpenCV库提供了强大的功能&#xff0c;可以方便地从视频中截取每一帧并将其保存为图片。这是一个很有趣的练习&#xff0c;可以让你更深入地了解图像处理和多媒体操作。 使用OpenCV库&#xff0c;你可以轻松地读取视频文件&#xff0…

判断推理 -- 图形推理 -- 位置规律

一组图&#xff1a;从前往后找规律。 二组图&#xff1a;从第一组图找规律&#xff0c;第二组图应用规律。 九宫格&#xff1a; 90%横着看找规律&#xff0c;第一行找规律&#xff0c;第二行验证规律&#xff0c;第三行应用规律。 所有有元素组成都是线&#xff0c;三角形&…

面试热题(验证二叉搜索树)

给你一个二叉树的根节点 root &#xff0c;判断其是否是一个有效的二叉搜索树。 有效 二叉搜索树定义如下&#xff1a; 节点的左子树只包含 小于 当前节点的数。节点的右子树只包含 大于 当前节点的数。所有左子树和右子树自身必须也是二叉树 二叉树满足以上3个条件&#xff0c…

spark的使用

spark的使用 spark是一款分布式的计算框架&#xff0c;用于调度成百上千的服务器集群。 安装pyspark # os.environ[PYSPARK_PYTHON]解析器路径 pyspark_python配置解析器路径 import os os.environ[PYSPARK_PYTHON]"D:/dev/python/python3.11.4/python.exe"pip inst…

喜盈门、梦百合竞相入局,智能床垫起风了

配图来自Canva可画 现代人的生活压力普遍大&#xff0c;熬夜、失眠是常有的事&#xff0c;提高睡眠质量十分的重要。近些年来&#xff0c;市面上出现了许多辅助睡眠的产品&#xff0c;比如香薰、褪黑素、蒸汽眼罩、降噪耳塞、助眠枕、睡眠监测app等助眠神器。可以说为了睡个好…

【CLion + ROS2】在 clion 中编译调试 ros2 package

目录 0 背景1. 命令行编译 ros2 package2. 使用 clion 打开 ros2 工程3. 使用 clion 编译整个 ros2 工程3.1 使用 clion 的 external tool 配置 colcon build3.2 开始编译 dev_ws 工程3.3 编译结果&#xff1a; 4. 调试单独的 ros2 package4.1 创建 ros2 package 的独立的 colc…

【Git】版本控制器详解之git的概念和基本使用

版本控制器git 初始Gitgit的安装git的基本使用初始化本地仓库配置本地仓库三区协作添加---add修改文件--status|diff版本回退--reset撤销修改删除文件 初始Git 为了能够更⽅便我们管理不同版本的⽂件&#xff0c;便有了版本控制器。所谓的版本控制器&#xff0c;就是⼀个可以记…

yolo源码注释2——数据集配置文件

代码基于yolov5 v6.0 目录&#xff1a; yolo源码注释1——文件结构yolo源码注释2——数据集配置文件yolo源码注释3——模型配置文件yolo源码注释4——yolo-py 数据集配置文件一般放在 data 文件夹下的 XXX.yaml 文件中&#xff0c;格式如下&#xff1a; path: # 数据集存放路…