用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.rhkb.cn/news/90503.html

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

相关文章

SpringBoot基于Zookeeper实现分布式锁

文章目录 问题背景前言实现搭建Zookeeper容器引入依赖ZK客户端的配置类ZK客户端的工厂类注入bean构建测试类 问题背景 研究分布式锁&#xff0c;基于ZK实现&#xff0c;需要整合到SpringBoot使用 前言 参考自SpringBoot集成Curator实现Zookeeper基本操作&#xff0c;Zookeeper入…

地理数据的双重呈现:GIS与数据可视化

前一篇文章带大家了解了GIS与三维GIS的关系&#xff0c;本文就GIS话题带大家一起探讨一下GIS和数据可视化之间的关系。 GIS&#xff08;地理信息系统&#xff09;和数据可视化在地理信息科学领域扮演着重要的角色&#xff0c;它们之间密切相关且相互增强。GIS是一种用于采集、…

Scrum敏捷模型的三个角色!如何在线绘制Scrum敏捷模型图?

1. 什么是Scrum敏捷模型&#xff1f; Scrum是一种敏捷开发方法&#xff0c;用于管理和组织软件开发项目。它强调团队的自组织和迭代式开发&#xff0c;通过不断的反馈和调整来快速交付高质量的软件产品。 Scrum敏捷模型将项目分解为一系列短期的迭代周期&#xff0c;每一个…

【量化课程】08_1.机器学习量化策略基础实战

文章目录 1. 常用机器学习模型1.1 回归模型1.2 分类模型1.2.1 SVC介绍1.2.2 SVC在量化策略中的应用 2. 机器学习量化策略实现的基本步骤3. 策略实现 1. 常用机器学习模型 1.1 回归模型 线性回归多层感知器回归自适应提升树回归随机森林回归 1.2 分类模型 线性分类支持向量机…

虹科方案 | 汽车总线协议转换解决方案(二)

上期说到&#xff0c;虹科的PCAN-LIN网关在CAN、LIN总线转换方面有显著的作用&#xff0c;尤其是为BMS电池通信的测试提供了优秀的解决方案。假如您感兴趣&#xff0c;可以点击文末相关链接进行回顾&#xff01; 而今天&#xff0c;虹科将继续给大家带来Router系列在各个领域的…

【Linux操作系统】编译过程中遇到的问题-为什么加-c?执行文件提示无法执行二进制文件?main函数参数argc和*argv[]的作用和理解?

在使用GCC编译器进行程序开发时&#xff0c;我们经常会遇到一些编译过程中的问题&#xff0c; 比如为什么要加上"-c"选项&#xff0c;以及为什么生成的可执行文件无法执行等问题。 本篇博客将详细介绍这些问题&#xff0c;并给出相应的代码和解释&#xff0c;帮助读者…

Python web实战之Django的AJAX支持详解

关键词&#xff1a;Web开发、Django、AJAX、前端交互、动态网页 今天和大家分享Django的AJAX支持。AJAX可实现在网页上动态加载内容、无刷新更新数据的需求。 1. AJAX简介 AJAX&#xff08;Asynchronous JavaScript and XML&#xff09;是一种在网页上实现异步通信的技术。通过…

20230814让惠普(HP)锐14 新AMD锐龙电脑不联网进WIN11进系统

20230814让惠普(HP)锐14 新AMD锐龙电脑不联网进WIN11进系统 2023/8/14 17:19 win11系统无法跳过联网 https://www.xpwin7.com/jiaocheng/28499.html Win11开机联网跳过不了怎么办&#xff1f;Win11开机联网跳过不了解决方法 Win11开机联网跳过不了怎么办&#xff1f;Win11开机…

8.14 ARM

1.练习一 .text 文本段 .global _start 声明一个_start函数入口 _start: _start标签&#xff0c;相当于C语言中函数mov r0,#0x2mov r1,#0x3cmp r0,r1beq stopsubhi r0,r0,r1subcc r1,r1,r0stop: stop标签&#xff0c;相当于C语言中函数b stop 跳转到stop标签下的第一条…

HCIP-OpenStack

1、OpenStack概述 OpenStack是一种云操作系统&#xff0c;OpenStack是虚拟机、裸金属和容器的云基础架构。可控制整个数据中心的大型计算、存储和网络资源池&#xff0c;所有资源都通过API或Web界面进行管理。 为什么称OpenStack是云操作系统&#xff1f; 云一般指云计算&…

【MySQL--->数据类型】

文章目录 [TOC](文章目录) 一、数据类型分类二、整型类型三、bit(位)类型四、float类型五、decimal类型六、char和varchar类型1.char类型2.varchar3.char与varchar的区别 七、日期与时间类型八、enum和set 一、数据类型分类 二、整型类型 数值类型有数据存储上限,而且每个类型都…

C语言暑假刷题冲刺篇——day1

目录 一、选择题 二、编程题 &#x1f388;个人主页&#xff1a;库库的里昂 &#x1f390;CSDN新晋作者 &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏✨收录专栏&#xff1a;C语言每日一练 ✨其他专栏&#xff1a;代码小游戏C语言初阶&#x1f91d;希望作者的文章能对你…

【计算机视觉|生成对抗】改进的生成对抗网络(GANs)训练技术

本系列博文为深度学习/计算机视觉论文笔记&#xff0c;转载请注明出处 标题&#xff1a;Improved Techniques for Training GANs 链接&#xff1a;[1606.03498v1] Improved Techniques for Training GANs (arxiv.org) 摘要 本文介绍了一系列应用于生成对抗网络&#xff08;G…

数据可视化工具的三大类报表制作流程分享

电脑&#xff08;pc&#xff09;、移动、大屏三大类型的BI数据可视化报表制作步骤基本相同&#xff0c;差别就在于尺寸调整和具体的报表布局。这对于采用点击、拖拉拽方式来制作报表的奥威BI数据可视化工具来说就显得特别简单。接下来&#xff0c;我们就一起看看不这三大类型的…

ES中倒排索引机制

在ES的倒排索引机制中有四个重要的名词&#xff1a;Term、Term Dictionary、Term Index、Posting List。 Term&#xff08;词条&#xff09;&#xff1a;词条是索引里面最小的存储和查询单元。一段文本经过分析器分析以后就会输出一串词条。一般来说英文语境中词条是一个单词&a…

wps设置一键标题字体和大小

参考 wps设置一键标题字体和大小&#xff1a;https://www.kafan.cn/A/7v5le1op3g.html 统一一键设置

数据结构:力扣刷题

题一&#xff1a;删除有序数组中的重复项 给你一个 升序排列 的数组 nums &#xff0c;请你 原地 删除重复出现的元素&#xff0c;使每个元素 只出现一次 &#xff0c;返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。 考虑 nums 的…

Intel汇编和ATT汇编的区别?

一、前缀不同 在 Intel 语法中&#xff0c;没有寄存器前缀或立即前缀。 然而&#xff0c;在 AT&T 中&#xff0c;寄存器的前缀是“%”&#xff0c;而 immed 的前缀是“$”。 Intel 语法十六进制或二进制即时数据分别带有“h”和“b”后缀。 此外&#xff0c;如果第一个十六…

14k字综述视觉大模型

目录 0.导读1.背景介绍1.1基础架构1.2目标函数1.2.1对比式学习1.2.2生成式学习1.3预训练1.3.1预训练数据集1.3.2微调1.3.3提示工程2.基于文本提示的基础模型2.1基于对比学习的方法2.1.1基于通用模型的对比方法2.1.2基于视觉定位基础模型的方法2.2基于生成式的方法2.3基于对比学…

安全加密框架图——Oracle安全开发者

Oracle安全开发者 ACLs 设计 ACLs&#xff08;访问控制列表&#xff09;时&#xff0c;可以根据以下思路进行设计&#xff1a; 所有者文件权限&#xff1a;确定文件的所有者能够对文件执行哪些操作&#xff0c;如读取、写入、执行等。这可以根据文件的性质和拥有者的职责来决…