将顽鹿运动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("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文件做简单记录。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值