将顽鹿运动fit文件同步至捷安特骑行

自行车买的是捷安特的,之前用手机当码表,记录用的是捷安特的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("", 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="" xmlns:xsi="" xsi:schemaLocation=" "> <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文件做简单记录。

没有编程基础但需要使用的朋友,请留言视情况做打包。

2025-01-10 21:10 点击量:2