用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.pswp.cn/news/37843.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/37843.shtml
英文地址,請注明出處:http://en.pswp.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: # 數據集存放路…