用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
循环干到底,所有的数据都会被push
到records
数组中。
返回的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
相关文章:

用Node.js吭哧吭哧撸一个运动主页
简单唠唠 某乎问题:人这一生,应该养成哪些好习惯? 问题链接:https://www.zhihu.com/question/460674063 如果我来回答肯定会有定期运动的字眼。 平日里也有煅练的习惯,时间久了后一直想把运动数据公开,…...

【C++】STL---vector
STL---vector 一、vector 的介绍二、vector 的模拟实现1. 容量相关的接口(1)size(2)capacity(3)reserve(4)resize(5)empty 2. [] 重载3. 迭代器4. 修改数据相…...

机器学习:基本介绍
机器学习介绍 Hnad-crafted rules Hand-crafted rules,叫做人设定的规则。那假设今天要设计一个机器人,可以帮忙打开或关掉音乐,那做法可能是这样: 设立一条规则,就是写一段程序。如果输入的句子里面看到**“turn of…...
基于长短期神经网络LSTM的碳排量预测,基于LSTM的碳排放量预测
目录 背影 摘要 LSTM的基本定义 LSTM实现的步骤 基于长短期神经网络LSTM的碳排放量预测 完整代码: 基于长短期神经网络LSTM的碳排放量预测,基于LSTM的碳排放量预测资源-CSDN文库 https://download.csdn.net/download/abc991835105/88184632 效果图 结果分析 展望 参考论文 背…...

日常BUG——SpringBoot关于父子工程依赖问题
😜作 者:是江迪呀✒️本文关键词:日常BUG、BUG、问题分析☀️每日 一言 :存在错误说明你在进步! 一、问题描述 在父子工程A和B中。A依赖于B,但是A中却无法引入B中的依赖,具体出现的…...

Zabbix监控tomcat
文章目录 一、安装部署TomcatTomcat二、安装Tomcat1.安装zabbix-agent收集监控数据(192.168.40.104)2.安装部署Zabbix-server(192.168.40.105)3.配置数据库 三、Zabbix监控Tomcat页面设置 实验环境 主机用途Centos7:192.168.40.105zabbix-server,zabbix-java-gatew…...

CentOS-6.3安装MySQL集群
安装要求 安装环境:CentOS-6.3 安装方式:源码编译安装 软件名称:mysql-cluster-gpl-7.2.6-linux2.6-x86_64.tar.gz 下载地址:http://mysql.mirror.kangaroot.net/Downloads/ 软件安装位置:/usr/local/mysql 数据存放位…...

项目管理的艺术:掌握成本效益分析
引言 在项目管理中,我们经常面临着如何有效地使用有限的资源来实现项目目标的挑战。为了解决这个问题,我们需要使用一种强大的工具——成本效益分析。通过成本效益分析,我们可以评估和比较不同的项目选项,选择最具成本效益的项目…...

护眼灯值不值得买?什么护眼灯对眼睛好
想要选好护眼台灯首先我们要知道什么是护眼台灯,大的方向来看,护眼台灯就是可以保护视力的台灯,深入些讲就是具备让灯发出接近自然光特性的光线,同时光线不会伤害人眼而出现造成眼部不适甚至是视力降低的照明设备。 从细节上看就…...

【设备树笔记整理4】内核对设备树的处理
1 从源头分析_内核head.S对dtb的简单处理 1.1 bootloader向内核传递的参数 (1)bootloader启动内核时,会设置r0,r1,r2三个寄存器: r0一般设置为0;r1一般设置为machine_id (在使用设备树时该参数没有被使用…...

算法通关村第七关——递归和迭代实现二叉树前中后序遍历
1.递归 1.1 熟悉递归 所有的递归有两个基本特征: 执行时范围不断缩小,这样才能触底反弹。终止判断在调用递归的前面。 写递归的步骤: 从小到大递推。分情况讨论,明确结束条件。组合出完整方法。想验证就从大到小画图推演。 …...

Datawhale Django后端开发入门Task01 Vscode配置环境
首先呢放一张运行成功的截图纪念一下,感谢众多小伙伴的帮助呀,之前没有配置这方面的经验 ,但还是一步一步配置成功了,所以在此以一个纯小白的经验分享如何配置成功。 1.选择要建立项目的文件夹,打开文件找到目标文件夹…...
django部署到centos服务器上
具体的操作步骤 步骤一 更新系统和安装依赖, sudo yum update sudo yum install python3 python3-pip python3-devel git步骤二:创建并激活虚拟环境 在终端中执行以下命令: python3 -m venv myenv source myenv/bin/activate可以不创建虚拟…...

IOS开发-XCode14介绍与入门
IOS开发-XCode14介绍与入门 1. XCODE14的小吐槽2. XCODE的功能bar一览3. XCODE项目配置一览4. XCODE更改DEBUG/RELEASE模式5. XCODE单元测试 1. XCODE14的小吐槽 iOS开发工具一直有个毛病,就是新版本的开发工具的总会有一些奇奇怪怪的bug。比如在我的Mac-Pro&#…...
Interactive Marker Publish Pose All the Time (Interactive Marker通过topic一直发送其状态)
以下代码实现了:Interactive Marker通过topic一直发送其状态,而不只是交互时才发送。 几个要点: 通过定时器rospy.Timer实现PublishInteractiveMarkerServer feedback.pose的类型是geometry_msgs/Pose,而不是geometry_msgs/PoseS…...

前后端分离------后端创建笔记(04)前后端对接
本文章转载于【SpringBootVue】全网最简单但实用的前后端分离项目实战笔记 - 前端_大菜007的博客-CSDN博客 仅用于学习和讨论,如有侵权请联系 源码:https://gitee.com/green_vegetables/x-admin-project.git 素材:https://pan.baidu.com/s/…...

一站式自动化测试平台-Autotestplat
3.1 自动化平台开发方案 3.1.1 功能需求 3.1.3 开发时间计划 如果是刚入门、但有一点代码基础的测试人员,大概 3 个月能做出演示版(Demo)进行自动化测试,6 个月内胜任开展工作中项目的自动化测试。 如果是有自动化测试基础的测试人员,大概 …...
Ansible Service模块,使用 Ansible Service模块进行服务管理
Ansible 是一种自动化工具,它可以简化配置管理、应用程序部署和任务自动化等操作。Ansible 的 Service 模块是其中一个重要的模块,它提供了管理服务的功能,使得在远程主机上启动、停止、重启和重新加载服务变得简单和可靠。本文将介绍 Ansibl…...

共识算法初探
共识机制的背景 加密货币都是去中心化的,去中心化的基础就是P2P节点众多,那么如何吸引用户加入网络成为节点,有那些激励机制?同时,开发的重点是让多个节点维护一个数据库,那么如何决定哪个节点写入&#x…...
Oracle查询表字段名并拼接
在数据库使用中,我们常常需要,获取一张表的全部字段,那该如何查询呢? 查询表字段名 SELECT column_name FROM all_tab_columns WHERE table_name table_name; 只需将引号中的table_name,替换为自己的表名࿰…...

Linux 文件类型,目录与路径,文件与目录管理
文件类型 后面的字符表示文件类型标志 普通文件:-(纯文本文件,二进制文件,数据格式文件) 如文本文件、图片、程序文件等。 目录文件:d(directory) 用来存放其他文件或子目录。 设备…...

【JavaEE】-- HTTP
1. HTTP是什么? HTTP(全称为"超文本传输协议")是一种应用非常广泛的应用层协议,HTTP是基于TCP协议的一种应用层协议。 应用层协议:是计算机网络协议栈中最高层的协议,它定义了运行在不同主机上…...

React19源码系列之 事件插件系统
事件类别 事件类型 定义 文档 Event Event 接口表示在 EventTarget 上出现的事件。 Event - Web API | MDN UIEvent UIEvent 接口表示简单的用户界面事件。 UIEvent - Web API | MDN KeyboardEvent KeyboardEvent 对象描述了用户与键盘的交互。 KeyboardEvent - Web…...

九天毕昇深度学习平台 | 如何安装库?
pip install 库名 -i https://pypi.tuna.tsinghua.edu.cn/simple --user 举个例子: 报错 ModuleNotFoundError: No module named torch 那么我需要安装 torch pip install torch -i https://pypi.tuna.tsinghua.edu.cn/simple --user pip install 库名&#x…...

让回归模型不再被异常值“带跑偏“,MSE和Cauchy损失函数在噪声数据环境下的实战对比
在机器学习的回归分析中,损失函数的选择对模型性能具有决定性影响。均方误差(MSE)作为经典的损失函数,在处理干净数据时表现优异,但在面对包含异常值的噪声数据时,其对大误差的二次惩罚机制往往导致模型参数…...

排序算法总结(C++)
目录 一、稳定性二、排序算法选择、冒泡、插入排序归并排序随机快速排序堆排序基数排序计数排序 三、总结 一、稳定性 排序算法的稳定性是指:同样大小的样本 **(同样大小的数据)**在排序之后不会改变原始的相对次序。 稳定性对基础类型对象…...

无人机侦测与反制技术的进展与应用
国家电网无人机侦测与反制技术的进展与应用 引言 随着无人机(无人驾驶飞行器,UAV)技术的快速发展,其在商业、娱乐和军事领域的广泛应用带来了新的安全挑战。特别是对于关键基础设施如电力系统,无人机的“黑飞”&…...

接口自动化测试:HttpRunner基础
相关文档 HttpRunner V3.x中文文档 HttpRunner 用户指南 使用HttpRunner 3.x实现接口自动化测试 HttpRunner介绍 HttpRunner 是一个开源的 API 测试工具,支持 HTTP(S)/HTTP2/WebSocket/RPC 等网络协议,涵盖接口测试、性能测试、数字体验监测等测试类型…...

(一)单例模式
一、前言 单例模式属于六大创建型模式,即在软件设计过程中,主要关注创建对象的结果,并不关心创建对象的过程及细节。创建型设计模式将类对象的实例化过程进行抽象化接口设计,从而隐藏了类对象的实例是如何被创建的,封装了软件系统使用的具体对象类型。 六大创建型模式包括…...

实战三:开发网页端界面完成黑白视频转为彩色视频
一、需求描述 设计一个简单的视频上色应用,用户可以通过网页界面上传黑白视频,系统会自动将其转换为彩色视频。整个过程对用户来说非常简单直观,不需要了解技术细节。 效果图 二、实现思路 总体思路: 用户通过Gradio界面上…...