用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 } = 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

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;
    });
}

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

[
  {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 = 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方法会将跑步、骑行数据汇总成这样:

[
  {
     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)) 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
    })
}

得到的数据是这样的:

[
  {
    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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Ant Design Mobile是什么?

在当今的数字时代&#xff0c;移动应用程序和网页设计已经成为各行各业的重要组成部分。用户界面的设计直接影响到用户体验和产品的成功。为了帮助设计师在移动设计领域更好&#xff0c;Antdesignmobile应运而生。Antdesignmobile是蚂蚁金服的移动UI设计语言和框架&#xff0c;…

网络通信TCP/IP协议逐层分析数据链路层(第四十课)

Ethernet Ⅱ帧,也称为Ethernet V2帧,是如今局域网里最常见的以太帧,是以太网事实标准。如今大多数的TCP/IP应用(如HTTP、FTP、SMTP、POP3等)都是采用Ethernet II帧承载。 1、MAC地址概述 -MAC地址,即以太网地址,用来标识一个以太网上的某个单独设备或一组设备 -长度…

认识excel篇3之数据的有效性(数据验证)

数据有效性不仅能够对单元格的输入数据进行条件限制&#xff0c;还可以在单元格中创建下拉列表菜单方便用户选择输入。如果没有做数据验证&#xff0c;单元格内默认可以输入任意类型的数据。数据验证就是限制单元格输入数据&#xff08;必须输入符合要求的才能输入&#xff09;…

Spring Boot+Redis 实现消息队列实践示例

Spring BootRedis 实现一个轻量级的消息队列 文章目录 Spring BootRedis 实现一个轻量级的消息队列0.前言1.基础介绍2.步骤2.1. 引入依赖2.2. 配置文件2.3. 核心源码 4.总结答疑 5.参考文档6. Redis从入门到精通系列文章 0.前言 本文将介绍如何利用Spring Boot与Redis结合实现…

Docker容器与虚拟化技术:Docker资源控制、数据管理

目录 一、理论 1.资源控制 2.Docker数据管理 二、实验 1.Docker资源控制 2.Docker数据管理 三、问题 1.docker容器故障导致大量日志集满&#xff0c;造成磁盘空间满 2、当日志占满之后如何处理 四、总结 一、理论 1.资源控制 (1) CPU 资源控制 cgroups&#xff0…

数据结构:堆的实现

1.堆的概念 如果有一个关键码的集合 K { k1 &#xff0c;k2 &#xff0c;k3 &#xff0c;…&#xff0c;kn }&#xff0c;把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中&#xff0c;并且 k(i) < k(i*21) 和 k(i) < k(i*22)&#xff0c; i 0 &#xff…

BUUCTF [安洵杯 2019]easy_serialize_php 1 详细讲解

题目来自buuctf&#xff0c;这是一题关于php序列化逃逸的题 1. 题目 题目给出的代码 <?php$function $_GET[f];function filter($img){$filter_arr array(php,flag,php5,php4,fl1g);$filter /.implode(|,$filter_arr)./i;return preg_replace($filter,,$img); }if($_S…

炬芯科技发布全新第二代智能手表芯片,引领腕上新趋势!

2023年7月&#xff0c;炬芯科技宣布全新第二代智能手表芯片正式发布。自2021年底炬芯科技推出第一代的智能手表芯片开始便快速获得了市场广泛认可和品牌客户的普遍好评。随着技术的不断创新和突破&#xff0c;为了更加精准地满足市场多元化的变幻和用户日益增长的体验需求&…

localhost:8080 is already in use

报错原因&#xff1a;本机的8080端口号已经被占用。因为机器的空闲端口号是随机分配的&#xff0c;而idea默认启动的端口号是8080,所以是存在这种情况。 对于这个问题&#xff0c;我们只需要重启idea或者修改项目的启动端口号即可。 更推荐第二种。对于修改项目启动端口号&…

vscode | linux | c++ intelliense 被弃用解决方案

每日一句&#xff0c;vscode用的爽是爽&#xff0c;主要是可配置太强了。如果也很会研究&#xff0c;可以直接去咸鱼接单了 废话少说&#xff0c;直接整。 用着用着说是c intelliense被弃用&#xff0c;很多辅助功能无法使用&#xff0c;像查看定义、查看引用、函数跳转、智能提…

Unity智慧园区夜景制作

近期使用Unity做了一个智慧园区场景的demo&#xff0c;初步了解了3D开发的一些步骤和知识&#xff0c;以下为制作的步骤&#xff0c;比较简略&#xff0c;备忘&#xff1a; 1. 制作前的设计分析&#xff1a; 1. 分析日光角度&#xff0c;阴影长度&#xff0c;效果 2. 分析冷暖…

前后端分离------后端创建笔记(10)用户修改

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

linux——mysql的高可用MHA

目录 一、概述 一、概念 二、组成 三、特点 四、工作原理 二、案例 三、构建MHA 一、基础环境 二、ssh免密登录 三、主从复制 master slave1 四、MHA安装 一、环境 二、安装node 三、安装manager 一、概述 一、概念 MHA&#xff08;MasterHigh Availability&a…

并发编程系列-CompletableFuture

利用多线程来提升性能&#xff0c;实质上是将顺序执行的操作转化为并行执行。仔细观察后&#xff0c;你还会发现在顺序转并行的过程中&#xff0c;一定会牵扯到异步化。举个例子&#xff0c;现在下面这段示例代码是按顺序执行的&#xff0c;为了优化性能&#xff0c;我们需要将…

哈工大开源“活字”对话大模型

一、介绍 大规模语言模型&#xff08;LLM&#xff09;在自然语言处理的通用领域已取得了令人瞩目的成功。对于广泛的应用场景&#xff0c;这种技术展示了强大的潜力&#xff0c;学术界和工业界的兴趣也持续升温。哈工大自然语言处理研究所30余位老师和学生参与开发了通用对话大…

数据库概述、部署MySQL服务、必备命令、密码管理、安装图形软件、SELECT语法 、筛选条件

Top NSD DBA DAY01 案例1&#xff1a;构建MySQL服务器案例2&#xff1a;密码管理案例3&#xff1a;安装图形软件案例4&#xff1a;筛选条件 1 案例1&#xff1a;构建MySQL服务器 1.1 问题 在IP地址192.168.88.50主机和192.168.88.51主机上部署mysql服务练习必备命令的使用 …

性能测试压力曲线模型分析

性能测试模压力曲线&#xff1a; 曲线图关键点介绍&#xff1a; 横轴&#xff1a;从左到右表现了Number of Concurrent Users&#xff08;并发用户数&#xff09;的不断增长。 纵轴&#xff1a;分别表示Utilization&#xff08;资源的利用情况&#xff0c;包括硬件资源和软件…

Android Framework 动态更新插拔设备节点执行权限

TF卡设备节点是插上之后动态添加&#xff0c;所以不能通过初始化设备节点权限来解决&#xff0c;需要监听TF插入事件&#xff0c;在init.rc 监听插入后动态更新设备节点执行权限 添加插拔TF卡监听 frameworks/base/services/core/java/com/android/server/StorageManagerServic…

Vue3组件库

Vue组件库 ViteVue3TypescriptTSX 1、项目搭建 1.1、创建项目&#xff08;yarn&#xff09; D:\WebstromProject>yarn create vite yarn create v1.22.19 [1/4] Resolving packages... [2/4] Fetching packages... [3/4] Linking dependencies... [4/4] Building fresh pa…