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

简单唠唠

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

问题链接:www.zhihu.com/question/46...

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

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

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

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

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

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

我的博客:www.linglan01.cn

我的运动主页:www.linglan01.cn/c/keep/inde...

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

梳理思路

平时跑步、骑行这两项活动多,所以我只需要调用这两个接口,再调用这两个接口前需要先登录获取到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之正、反向代理》

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

运动数据总和

请求跑步接口方法:

getRunning.js文件链接github.com/CatsAndMice...

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文件链接 github.com/CatsAndMice...

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文件链接github.com/CatsAndMice...

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文件链接 github.com/CatsAndMice...

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

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

json 复制代码
[
  {year:2023,kmDistance:99},
  {year:2022,kmDistance:66},
  //...
]

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

骑行计算总和:

cycling.js文件链接github.com/CatsAndMice...

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文件链接 github.com/CatsAndMice...

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文件链接github.com/CatsAndMice...

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

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

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

js 复制代码
//省略
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文件链接 github.com/CatsAndMice...

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

得到的数据是这样的:

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

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

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

js 复制代码
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服务 》文章链接linglan01.cn/post/47

index.js文件链接github.com/CatsAndMice...

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('服务已开启');
})

最后的话

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

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

又完成了一篇文章,奖励自己一顿火锅。

如果我的文章对你有帮助,您的👍就是对我的最大支持^_^。

更多文章:linglan01.cn/about

相关推荐
姚*鸿的博客5 分钟前
pinia在vue3中的使用
前端·javascript·vue.js
宇文仲竹24 分钟前
edge 插件 iframe 读取
前端·edge
Kika写代码27 分钟前
【基于轻量型架构的WEB开发】【章节作业】
前端·oracle·架构
苹果酱05671 小时前
一文读懂SpringCLoud
java·开发语言·spring boot·后端·中间件
掐指一算乀缺钱1 小时前
SpringBoot 数据库表结构文档生成
java·数据库·spring boot·后端·spring
天下无贼!2 小时前
2024年最新版Vue3学习笔记
前端·vue.js·笔记·学习·vue
Jiaberrr2 小时前
JS实现树形结构数据中特定节点及其子节点显示属性设置的技巧(可用于树形节点过滤筛选)
前端·javascript·tree·树形·过滤筛选
赵啸林2 小时前
npm发布插件超级简单版
前端·npm·node.js
我码玄黄2 小时前
THREE.js:网页上的3D世界构建者
开发语言·javascript·3d
罔闻_spider2 小时前
爬虫----webpack
前端·爬虫·webpack