自行车买的是捷安特的,之前用手机当码表,记录用的是捷安特的app。
后面买了迈金的码表,个人感觉顽鹿运动做的运动记录分析没有捷安特的适合自己,而且已经有一部分数据在捷安特无法同步到顽鹿运动,好在顽鹿运动可以下载fit文件同步到捷安特骑行。
顽鹿运动fit文件可以从网站(顽鹿运动访问地址)下载好导入到捷安特网站(捷安特骑行访问地址)。
久而久之,同步骑行记录变成了日常,感觉甚是乏味。好在自己是个程序员,乏味的事情,那就交给程序来做吧。
Java项目已上传至Github,供想要下载源代码的朋友下载(Github访问地址)。
附核心代码:
Main.java
package com.dream.mryang.syncTheRecordingOfOnelapToGiant;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.dream.mryang.syncTheRecordingOfOnelapToGiant.utils.HttpClientUtil;
import com.dream.mryang.syncTheRecordingOfOnelapToGiant.utils.TxtOperationUtil;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.http.NameValuePair;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author yang
* @since 2024/8/28
**/
public class Main {
private final static ContentType CONTENT_TYPE = ContentType.create(HTTP.PLAIN_TEXT_TYPE, HTTP.UTF_8);
/**
* 顽鹿运动账号
*/
private static final String ONELAP_ACCOUNT = "";
/**
* 顽鹿运动密码
*/
private static final String ONELAP_PASSWORD = "";
/**
* 捷安特骑行账号
*/
private static final String GIANT_USERNAME = "";
/**
* 捷安特骑行密码
*/
private static final String GIANT_PASSWORD = "";
/**
* 同步最近活动数量,约30天60次
*/
private static final Integer SYNC_RECENT_ACTIVITY_COUNT = 60;
/**
* 顽鹿运动fit文件存储目录
*/
private static final String ONELAP_FIT_FILE_STORAGE_DIRECOTRY = "W:\\onelapFitFileStorageDirecotry\\";
/**
* 已同步fit文件记录存储txt文件路径
*/
private static final String SYNC_FIT_FILE_SAVE_FILE_PATH = "W:\\onelapFitFileStorageDirecotry\\syncFitFileSaveFile.txt";
public static void main(String[] args) {
// 下载顽鹿运动fit文件
ArrayList<String> fitFileNameList = downloadTheOnelapFitFile();
// fit文件同步到捷安特骑行
if (CollectionUtils.isNotEmpty(fitFileNameList)) {
syncFitFilesToGiantBike(fitFileNameList);
}
System.out.println("已完成同步数量:" + fitFileNameList.size());
}
private static ArrayList<String> downloadTheOnelapFitFile() {
// 调 顽鹿运动登录 接口,获取登录信息
String loginReturnJsonString = HttpClientUtil.doPostJson("https://www.onelap.cn/api/login", "{\"account\":\"" + ONELAP_ACCOUNT + "\",\"password\":\"" + DigestUtils.md5Hex(ONELAP_PASSWORD) + "\"}", null, null);
// 输出 登录信息 Json字符串
System.out.println(loginReturnJsonString);
// 解析 登录信息 Json字符串
JSONObject loginReturnData = JSONObject.parseObject(loginReturnJsonString);
JSONArray data = loginReturnData.getJSONArray("data");
JSONObject loginData = data.getJSONObject(0);
// 解析出登录token1
String token = loginData.getString("token");
// 解析出登录token2
String refreshToken = loginData.getString("refresh_token");
// 解析出userinfo对象信息
JSONObject loginUserInfoData = loginData.getJSONObject("userinfo");
// 解析出uid
String uid = loginUserInfoData.getString("uid");
// 拼接cookie数据
String cookie = "ouid=" + uid + "; " +
"XSRF-TOKEN=" + token + "; " +
"OTOKEN=" + refreshToken;
// 调 我的活动 接口,获取活动记录
String myActivitiesJsonString = HttpClientUtil.doGet("http://u.onelap.cn/analysis/list", null, cookie);
// 解析 我的活动 Json字符串
JSONObject myActivitiesData = JSONObject.parseObject(myActivitiesJsonString);
// 获取 我的活动 列表数据
JSONArray myActivities = myActivitiesData.getJSONArray("data");
// 确认同步最近活动数量
int endIndex;
if (myActivities.size() >= SYNC_RECENT_ACTIVITY_COUNT) {
endIndex = SYNC_RECENT_ACTIVITY_COUNT;
} else {
endIndex = myActivities.size();
}
// 同步文件名称
ArrayList<String> syncFileName = new ArrayList<>();
// 将已经同步的文件过滤
List<Object> myActivitieObjectList = myActivities.stream().limit(endIndex).filter(a -> {
// 读取已同步文件
ArrayList<String> list = TxtOperationUtil.readTxtFile(SYNC_FIT_FILE_SAVE_FILE_PATH);
// 获取 我的活动 数据
JSONObject jsonObject = (JSONObject) JSONObject.toJSON(a);
// 解析出文件名
String fileKey = jsonObject.getString("fileKey");
return !list.contains(fileKey);
}).collect(Collectors.toList());
// 循环下载文件
for (Object myActivitieObject : myActivitieObjectList) {
// 获取 我的活动 数据
JSONObject jsonObject = (JSONObject) JSONObject.toJSON(myActivitieObject);
// 解析出文件名
String fileKey = jsonObject.getString("fileKey");
// 解析出下载地址
String durl = jsonObject.getString("durl");
// 创建下载存储文件对象
File file = new File(ONELAP_FIT_FILE_STORAGE_DIRECOTRY + fileKey);
// 发送下载文件请求
HttpClientUtil.doPostJson(durl, file);
syncFileName.add(fileKey);
}
return syncFileName;
}
public static void syncFitFilesToGiantBike(ArrayList<String> fitFileNameList) {
// 封装捷安特骑行登录参数
// 创建NameValuePair列表用于存储表单数据
List<NameValuePair> formParams = new ArrayList<>();
formParams.add(new BasicNameValuePair("username", GIANT_USERNAME));
formParams.add(new BasicNameValuePair("password", GIANT_PASSWORD));
// 调 捷安特骑行登录 接口,获取登录信息
String loginReturnJsonString = HttpClientUtil.doPostJson("https://ridelife.giant.com.cn/index.php/api/login", null, formParams, null);
// 解析 登录信息 Json字符串
JSONObject loginReturnData = JSONObject.parseObject(loginReturnJsonString);
// 解析出登录token1
String userToken = loginReturnData.getString("user_token");
// 封装捷安特骑行上传fit文件参数
MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
for (String fitFileName : fitFileNameList) {
File file = new File(ONELAP_FIT_FILE_STORAGE_DIRECOTRY + fitFileName);
multipartEntityBuilder.addBinaryBody("files[]", file, ContentType.DEFAULT_BINARY, file.getName());
}
multipartEntityBuilder.addPart("token", new StringBody(userToken, CONTENT_TYPE));
multipartEntityBuilder.addPart("device", new StringBody("bike_computer", CONTENT_TYPE));
multipartEntityBuilder.addPart("brand", new StringBody("onelap", CONTENT_TYPE));
// 调用接口上传文件
String respondJson = HttpClientUtil.doPostJson("https://ridelife.giant.com.cn/index.php/api/upload_fit", null, null, multipartEntityBuilder);
// 输出响应
System.out.println(respondJson);
// 解析 上传文件 Json字符串
JSONObject respondJsonData = JSONObject.parseObject(respondJson);
// 解析出登录token1
Integer status = respondJsonData.getInteger("status");
if (status == 1) {
// 存储同步文件记录
TxtOperationUtil.writeTxtFile(SYNC_FIT_FILE_SAVE_FILE_PATH, fitFileNameList);
} else {
throw new RuntimeException("调用接口上传文件响应异常,异常信息:" + respondJson);
}
}
}
HttpClientUtil.java
package com.dream.mryang.syncTheRecordingOfOnelapToGiant.utils;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Map;
/**
* @author yang
* @since 2024/8/29
**/
public class HttpClientUtil {
/**
* 发送post请求,传递formData参数
*
* @param url 请求地址
* @param formParams formData参数
*/
public static String doPostJson(String url, String json, List<NameValuePair> formParams, MultipartEntityBuilder filesMultipartEntityBuilder) {
// 创建Httpclient对象
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 创建请求Json内容
if (StringUtils.isNotBlank(json)) {
StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
httpPost.setEntity(entity);
}
// 创建UrlEncodedFormEntity实例
if (CollectionUtils.isNotEmpty(formParams)) {
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams, "UTF-8");
// 设置请求的实体为表单数据
httpPost.setEntity(entity);
}
// 传递文件
if (filesMultipartEntityBuilder != null) {
HttpEntity entity = filesMultipartEntityBuilder.build();
httpPost.setEntity(entity);
}
// 执行http请求
CloseableHttpResponse response = httpClient.execute(httpPost);
return EntityUtils.toString(response.getEntity(), "UTF-8");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 发送get请求
*
* @param url 请求地址
* @param param url参数
* @param cookie cookie值
*/
public static String doGet(String url, Map<String, String> param, String cookie) {
// 创建Httpclient对象
try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
// 创建uri
URIBuilder builder = new URIBuilder(url);
if (param != null) {
for (String key : param.keySet()) {
builder.addParameter(key, param.get(key));
}
}
URI uri = builder.build();
// 创建http GET请求
HttpGet httpGet = new HttpGet(uri);
// 封装cookie请求头
if (StringUtils.isNotBlank(cookie)) {
httpGet.setHeader("cookie", cookie);
}
// 执行请求
CloseableHttpResponse response = httpclient.execute(httpGet);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == 200) {
return EntityUtils.toString(response.getEntity(), "UTF-8");
} else {
throw new RuntimeException("判断返回状态不为200,请排查问题");
}
} catch (IOException | URISyntaxException e) {
throw new RuntimeException(e);
}
}
/**
* 发送post请求,获取文件流并保存
*
* @param url 请求地址
* @param savePath 保存文件全路径
*/
public static void doPostJson(String url, File savePath) {
// 创建Httpclient对象
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 执行http请求
CloseableHttpResponse response = httpClient.execute(httpPost);
HttpEntity httpEntity = response.getEntity();
byte[] data = EntityUtils.toByteArray(httpEntity);
//存入磁盘
try (FileOutputStream fos = new FileOutputStream(savePath)) {
fos.write(data);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
TxtOperationUtil.java
package com.dream.mryang.syncTheRecordingOfOnelapToGiant.utils;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* @author yang
* @since 2024/8/29
*/
public class TxtOperationUtil {
/**
* 根据文件路径读取文件
*
* @param filePath 文件路径
*/
public static ArrayList<String> readTxtFile(String filePath) {
try {
// 返回值集合
ArrayList<String> respondList = new ArrayList<>();
// 文件流对象
FileInputStream fileInputStream = new FileInputStream(filePath);
// 文件读取流对象
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, StandardCharsets.UTF_8);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
// 读取行数据中间变量
String line;
while ((line = bufferedReader.readLine()) != null) {
respondList.add(line);
}
// 关闭流
fileInputStream.close();
inputStreamReader.close();
// 返回数据对象
return respondList;
} catch (Exception e) {
throw new RuntimeException("读取txt文件异常,请检查后再试", e);
}
}
/**
* 写入文件
*
* @param filePath 文件路径
* @param textList 文件行内容
*/
public static void writeTxtFile(String filePath, List<String> textList) {
try (RandomAccessFile raf = new RandomAccessFile(new File(filePath), "rw")) {
byte[] originalContent = new byte[(int) raf.length()];
// 存储原记录
raf.read(originalContent);
// 将指针移到首行
raf.seek(0);
for (String textString : textList) {
raf.write(textString.getBytes());
// 添加换行
raf.write(("\n").getBytes());
}
raf.write(originalContent);
} catch (Exception e) {
throw new RuntimeException("数据写入txt文件异常,请检查后再试");
}
}
}
pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.dream.mryang</groupId>
<artifactId>synchronizeTheRecordingOfOnelapToGiant</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<!-- httpClient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.5.14</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>de.ntcomputer</groupId>
<artifactId>executable-packer-maven-plugin</artifactId>
<version>1.0.1</version>
<configuration>
<finalName>syncTheRecordingOfOnelapToGiant</finalName>
<mainClass>com.dream.mryang.syncTheRecordingOfOnelapToGiant.Main</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>pack-executable-jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
注意:需为静态变量赋值;创建【已同步fit文件记录存储txt文件路径】文件夹及文件。
未引入数据库,轻量化使用TXT文件做简单记录。
没有编程基础但需要使用的朋友,请留言视情况做打包。