展开菜单
首页 精品内容 本月促销 装机必备 Windows macOS软件 IOS软件 Android AI PDF教程 专题
全部分类

当前位置:

首页 > 编程开发 > Springboot集成Tile客户端之Set命令如何实现

Springboot集成Tile客户端之Set命令如何实现

set命令语法SETkeyid[FIELDnamevalue...][EXseconds][NX|XX](OBJECTgeojson)|(POINTlatlonz)|(BOUNDSminlatminlonmaxlatmaxlon)|(HASHgeohash)|(STRINGvalue)set命令就相当于redis中的hash命令的使用,也是一个key和id的组合,但是不同的是,Tile38的set命令还可以携带更多的其他属性,比如可以自定义FIELD字段,还可以设置EX有效期等等,那么我们需要给这个语法设

    set命令语法

    SET key id [FIELD name value ...] [EX seconds] [NX|XX] (OBJECT geojson)|(POINT lat lon z)|(BOUNDS minlat minlon maxlat maxlon)|(HASH geohash)|(STRING value)

    set命令就相当于redis中的hash命令的使用,也是一个keyid的组合,但是不同的是,Tile38的set命令还可以携带更多的其他属性,比如可以自定义FIELD字段,还可以设置EX有效期等等,那么我们需要给这个语法设计一套好用的java api,以便开发人员可以更好地使用Tile38。

    语法分析

    首先,根据上面提供的语法,我们可以分为三部分:

    1.第一部分就是命令的启示关键字SET,我们把这个关键字单独作为一部分;

    2.第二部分就是key id [FIELD name value ...] [EX seconds] [NX|XX],我们把这些都作为参数;

    3.第三部分就是最后的目标数据对象:

    (OBJECT geojson)|(POINT lat lon z)|(BOUNDS minlat minlon maxlat maxlon)|(HASH geohash)|(STRING value)

    代码设计

    1.我们把第一部分的命令关键字通过枚举的方式来管理:

    enum Tile38Command implements ProtocolKeyword {
        SET;
        public final byte[] bytes;
        static final String UNDERSCORE = "_";
        static final String SPACE = " ";
        Tile38Command() {
          String name = StringUtils.replace(this.name(), UNDERSCORE, SPACE);
          this.bytes = name.getBytes(StandardCharsets.US_ASCII);
        }
        @Override
        public byte[] getBytes() {
          return this.bytes;
        }
    }

    因为redis客户端工具在发送命令前需要对所有命令进行编码,所以要求所有的命令都必须实现ProtocolKeyword接口。如果命令的起始关键字是两个或多个单词,那么我们会使用下划线连接,转换成bytes的时候我们可以使用空格把下划线替换。

    2.我们把命令的第二部分抽象成一个具体的class,通过相关的字段来进行描述:

    public class SetOpts {
      private String key;
      private String id;
      //字段值必须是双精度浮点型
      private Map fields;
      // 单位秒
      private int ex;
      // 创建方式:
      // NX 不存在的时候创建
      // XX 存在的时候更新
      private NxXx nxXx;
      private SetOpts(Builder builder) {
        this.key = builder.key;
        this.id = builder.id;
        this.fields = builder.fields;
        this.ex = builder.ex;
        this.nxXx = builder.nxXx;
      }
      
      // 把所有的参数按顺序放到列表中
      public List commandLine() {
        List result = new LinkedList<>();
        result.add(this.key);
        result.add(this.id);
        // 添加所有的FIELD
        if (MapUtils.isNotEmpty(this.fields)) {
          for (Map.Entry entry : this.fields.entrySet()) {
            result.add("FIELD");
            result.add(entry.getKey());
            result.add(entry.getValue().toString());
          }
        }
        // 添加`EX`
        if (this.ex >= 0) {
          result.add("EX");
          result.add(String.valueOf(this.ex));
        }
        // 添加NX或XX
        if (Objects.nonNull(this.nxXx)) {
          result.add(this.nxXx.name());
        }
        // 返回结果
        return result;
      }
      
      public enum NxXx {
        NX,
        XX
      }
      // 建造者模式
      public static class Builder {
        private String key;
        private String id;
        //字段值必须是双精度浮点型
        private Map fields;
        // 单位秒
        private int ex = -1;
        // 创建方式:
        // NX 不存在的时候创建
        // XX 存在的时候更新
        private NxXx nxXx;
        public Builder key(String key) {
          this.key = key;
          return this;
        }
        public Builder id(String id) {
          this.id = id;
          return this;
        }
        public Builder field(String field, double value) {
          if (Objects.isNull(this.fields)) {
            this.fields = new LinkedHashMap<>();
          }
          this.fields.put(field, value);
          return this;
        }
        public Builder ex(int seconds) {
          this.ex = seconds;
          return this;
        }
        public Builder nxXx(NxXx nxXx) {
          this.nxXx = nxXx;
          return this;
        }
        public SetOpts build() throws AwesomeException {
          if (StringUtils.isEmpty(this.key)) {
            throw new AwesomeException(500, "key is empty");
          }
          if (StringUtils.isEmpty(this.id)) {
            throw new AwesomeException(500, "id is empty");
          }
          // 创建SetOpts对象
          return new SetOpts(this);
        }
      }
    }

    我们上面通过建造者的设计模式,把所有的参数都转换成了SetOpts这个类当中,开发人员就可以通过SetOpts对象的构建来灵活地控制命令中的参数了。

    3.我们需要把第三部分当中的不同数据对象转换成不同的类型:

    POINT数据类型

    Point关键的字段就是经纬度,除此之外,还有一个额外的字段z,用来存储额外的业务参数,可为空。

    public class Point extends Element implements Serializable {
      // 经度
      private double lng;
      // 维度
      private double lat;
      // 额外的数据
      private double z;
      public Point(double lng, double lat, double z) {
        this.lat = lat;
        this.lng = lng;
        this.z = z;
      }
      public Point(double lng, double lat) {
        this(lng, lat, Integer.MIN_VALUE);
      }
      @Override
      public List commandArgs() {
        List result = new LinkedList<>();
        result.add("POINT");
        result.add(String.valueOf(this.lng));
        result.add(String.valueOf(this.lat));
        if (this.z != Integer.MIN_VALUE) {
          result.add(String.valueOf(this.z));
        }
        return result;
      }
    }

    BOUNDS数据类型

    BOUNDS就是矩形,它的关键字段就是左下角和右上角两个点位,我们使用coordinate1和coordinate2来表示左下角和右上角;

    @AllArgsConstructor
    public class Bounds extends Element {
      private double[] coordinate1;
      private double[] coordinate2;
      @Override
      public List commandArgs() {
        List result = new LinkedList<>();
        result.add("BOUNDS");
        result.add(String.valueOf(coordinate1[0]));
        result.add(String.valueOf(coordinate1[1]));
        result.add(String.valueOf(coordinate2[0]));
        result.add(String.valueOf(coordinate2[1]));
        return result;
      }
    }

    HASH和STRING数据类型

    HASH和STRING其实就是一个单独的字符串,但是我们还是把它封装一下,以便开发人员使用;

    @AllArgsConstructor
    public class Geohash extends Element {
      private String hash;
      @Override
      public List commandArgs() {
        List result = new LinkedList<>();
        result.add("HASH");
        result.add(this.hash);
        return result;
      }
    }
    @AllArgsConstructor
    public class RawString extends Element {
      private String raw;
      @Override
      public List commandArgs() {
        List result = new LinkedList<>();
        result.add("STRING");
        result.add(this.raw);
        return result;
      }
    }

    OBJECT数据类型

    OBJECT其实就是GeoJSON数据,这一类数据比较复杂一点,一共有六种类型,想了解的小伙伴可以看这里geojson.org/

    Point,
    LineString,
    Polygon,
    MultiPoint,
    MultiLineString,
    MultiPolygon

    为了开发人员能够更好的使用这六种类型,我们同样使用建造者模式来设计一下GeoJSON数据类型:

    @Data
    public class GeoJson {
      public static class Builder {
        public Point.Builder point() {
          return new Point.Builder();
        }
        public MultiPoint.Builder multPoint() {
          return new MultiPoint.Builder();
        }
        public LineString.Builder lineString() {
          return new LineString.Builder();
        }
        public MultiLineString.Builder multiLineString() {
          return new MultiLineString.Builder();
        }
        public Polygon.Builder polygon() {
          return new Polygon.Builder();
        }
        public MultiPolygon.Builder multiPolygon() {
          return new MultiPolygon.Builder();
        }
      }
    }

    我们现在一个大类里面创建多个方法,每一个方法都把对应类型的建造者给创造出来,这样的话,就相当于这个类当中有创建六种对象的方式,每个建造者都只负责建造对应的那个对象。

    下面分别是六个建造者的代码,每个对象都基于最基本的BaseGeoJson来构造,BaseGeoJson中把公共的字段type和额外的meta字段抽出来,各个类型不同的点在于坐标点的数量和层次不同,所以根据各自类型的特点,代码设计如下:

    // Point类型
      public static class Point extends BaseGeoJson {
        // 坐标点
        private double[] coordinates;
        Point(Builder builder) {
          super(builder);
          this.type = GeoJsonType.Point;
          this.coordinates = builder.coordinates;
        }
        @Override
        protected Object coordinates() {
          return this.coordinates;
        }
        public static class Builder extends BaseGeoJson.Builder {
          private double[] coordinates;
          public Builder coordinate(double lon, double lat) {
            coordinates = new double[]{lat, lon};
            return this;
          }
          public Point build() {
            return new Point(this);
          }
        }
      }
    // MultiPoint类型
      public static class MultiPoint extends BaseGeoJson {
        private double[][] coordinates;
        MultiPoint(Builder builder) {
          super(builder);
          this.type = GeoJsonType.MultiPoint;
          this.coordinates = builder.convert2Array();
        }
        @Override
        protected Object coordinates() {
          return this.coordinates;
        }
        public static class Builder extends BaseGeoJson.Builder {
          private List coordinates;
          public Builder coordinate(double lon, double lat) {
            if (CollectionUtils.isEmpty(this.coordinates)) {
              this.coordinates = new LinkedList<>();
            }
            this.coordinates.add(new Coordinate(lat, lon));
            return this;
          }
          protected double[][] convert2Array() {
            int length = this.coordinates.size();
            double[][] result = new double[length][];
            for (int i = 0; i < length; i++) {
              result[i] = this.coordinates.get(i).convertToArray();
            }
            return result;
          }
          @Override
          public MultiPoint build() {
            return new MultiPoint(this);
          }
        }
      }
    // LineString类型
      public static class LineString extends MultiPoint {
        private double[][] coordinates;
        LineString(Builder builder) {
          super(builder);
          this.type = GeoJsonType.LineString;
        }
        public static class Builder extends MultiPoint.Builder {
          @Override
          public LineString build() {
            return new LineString(this);
          }
        }
      }
    // MultiLineString类型
      public static class MultiLineString extends BaseGeoJson {
        private double[][][] coordinates;
        MultiLineString(Builder builder) {
          super(builder);
          this.type = GeoJsonType.MultiLineString;
          this.coordinates = builder.convertToArray();
        }
        @Override
        protected Object coordinates() {
          return this.coordinates;
        }
        public static class Builder extends BaseGeoJson.Builder {
          private List lines = new LinkedList<>();
          public Line line() {
            return new Line(this);
          }
          void addLine(Line line) {
            lines.add(line);
          }
          double[][][] convertToArray() {
            int length = this.lines.size();
            double[][][] result = new double[length][][];
            for (int i = 0; i < length; i++) {
              Line line = this.lines.get(i);
              result[i] = line.convert2Array();
            }
            return result;
          }
          @Override
          public BaseGeoJson build() {
            return new MultiLineString(this);
          }
        }
        static class Line {
          private List coordinates;
          private Builder builder;
          Line(Builder builder) {
            this.builder = builder;
            this.builder.addLine(this);
          }
          private double[][] convert2Array() {
            int length = this.coordinates.size();
            double[][] result = new double[length][];
            for (int i = 0; i < length; i++) {
              result[i] = this.coordinates.get(i).convertToArray();
            }
            return result;
          }
          public Line coordinate(double lon, double lat) {
            if (CollectionUtils.isEmpty(this.coordinates)) {
              this.coordinates = new LinkedList<>();
            }
            this.coordinates.add(new Coordinate(lat, lon));
            return this;
          }
          public Line nextLine() {
            return new Line(this.builder);
          }
          public Builder end() {
            return this.builder;
          }
        }
      }
    // Polygon类型
      public static class Polygon extends MultiPoint {
        private double[][][] coordinates;
        Polygon(Builder builder) {
          super(builder);
          this.type = GeoJsonType.Polygon;
          this.coordinates = new double[][][]{builder.convert2Array()};
        }
        public static class Builder extends MultiPoint.Builder {
          @Override
          public Polygon build() {
            return new Polygon(this);
          }
        }
      }
    // MultiPolygon类型
      public static class MultiPolygon extends BaseGeoJson {
        private double[][][][] coordinates;
        MultiPolygon(Builder builder) {
          super(builder);
          this.type = GeoJsonType.MultiPolygon;
          this.coordinates = new double[][][][]{builder.convert2Array()};
        }
        @Override
        protected Object coordinates() {
          return this.coordinates;
        }
        public static class Builder extends BaseGeoJson.Builder {
          private List polygons = new LinkedList<>();
          @Override
          public BaseGeoJson build() {
            return new MultiPolygon(this);
          }
          void addPolygon(Polygon polygon) {
            polygons.add(polygon);
          }
          private double[][][] convert2Array() {
            int length = this.polygons.size();
            double[][][] result = new double[length][][];
            for (int i = 0; i < length; i++) {
              result[i] = this.polygons.get(i).convert2Array();
            }
            return result;
          }
        }
        static class Polygon {
          private List coordinates;
          private Builder builder;
          Polygon(Builder builder) {
            this.builder = builder;
            this.builder.addPolygon(this);
          }
          private double[][] convert2Array() {
            int length = this.coordinates.size();
            double[][] result = new double[length][];
            for (int i = 0; i < length; i++) {
              result[i] = this.coordinates.get(i).convertToArray();
            }
            return result;
          }
          public Polygon coordinate(double lon, double lat) {
            if (CollectionUtils.isEmpty(this.coordinates)) {
              this.coordinates = new LinkedList<>();
            }
            this.coordinates.add(new Coordinate(lat, lon));
            return this;
          }
          public Polygon nextLine() {
            return new Polygon(this.builder);
          }
          public Builder end() {
            return this.builder;
          }
        }
      }
    // 基类BaseGeoJson
      public abstract static class BaseGeoJson extends Element {
        // 公共字段type
        protected GeoJsonType type;
        // 公共字段metadata
        private Map metadata;
        BaseGeoJson(Builder builder) {
          this.metadata = builder.metadata;
        }
        protected abstract Object coordinates();
        // 转换成命令参数
        @Override
        public List commandArgs() {
          List result = new LinkedList<>();
          result.add("OBJECT");
          result.add(toJson());
          return result;
        }
        // 提供统一的转json方法
        protected String toJson() {
          Map map = new LinkedHashMap<>();
          map.put("type", this.type);
          map.put("coordinates", coordinates());
          if (!CollectionUtils.isEmpty(this.metadata)) {
            for (Map.Entry entry : this.metadata.entrySet()) {
              map.put(entry.getKey(), entry.getValue());
            }
          }
          return JsonUtil.obj2String(map);
        }
        abstract static class Builder {
          private Map metadata;
          public Builder meta(String key, String value) {
            if (MapUtils.isEmpty(this.metadata)) {
              this.metadata = new LinkedHashMap<>();
            }
            this.metadata.put(key, value);
            return this;
          }
          public abstract BaseGeoJson build();
        }
        static class Coordinate {
          private double lat;
          private double lon;
          Coordinate(double lat, double lon) {
            this.lat = lat;
            this.lon = lon;
          }
          public double[] convertToArray() {
            return new double[]{this.lat, this.lon};
          }
        }
        // GeoJSON所有的数据类型
        enum GeoJsonType {
          Point,
          LineString,
          Polygon,
          MultiPoint,
          MultiLineString,
          MultiPolygon
        }
      }

    最后,再补充一个基类Element:

    public abstract class Element implements Serializable {
      public abstract List commandArgs();
    }

    如何使用

    我们针对所有的数据类型全部转换成具体的代码设计,下面我们看看如何使用:

    private String setElement(SetOpts setOpts, Element element) {
        List args1 = setOpts.commandLine();
        List commandArgs = element.commandArgs();
        return execute(Tile38Command.SET, args1, commandArgs);
    }
    /**
       * 设置点位
       *
       * @param setOpts
       * @param point
       * @return
       */
      public String setPoint(SetOpts setOpts, Point point) {
        return setElement(setOpts, point);
      }
      /**
       * 设置对象
       *
       * @param setOpts
       * @param geoJson
       * @return
       */
      public String setObject(SetOpts setOpts, GeoJson.BaseGeoJson geoJson) {
        return setElement(setOpts, geoJson);
      }
      /**
       * 设置矩形边界
       *
       * @param setOpts
       * @param bounds
       * @return
       */
      public String setBounds(SetOpts setOpts, Bounds bounds) {
        return setElement(setOpts, bounds);
      }
      /**
       * 设置geohash
       *
       * @param setOpts
       * @param geohash
       * @return
       */
      public String setGeohash(SetOpts setOpts, Geohash geohash) {
        return setElement(setOpts, geohash);
      }
      /**
       * 设置String
       *
       * @param setOpts
       * @param string
       * @return
       */
      public String setString(SetOpts setOpts, RawString string) {
        return setElement(setOpts, string);
      }
    本文内容来源于互联网,如有侵权请联系删除。
    作者最新文章
    编程开发
    相关文章 更多
    精品专题 更多
    本月促销

    正软商城本月促销专区,汇集办公、设计、安全、影音、系统工具及AI软件等正版软件优惠活动,提供限时折扣、特价授权和优惠购买信息,活动库存及价格以页面实时展示为准。

    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    macOS软件

    正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

    IOS软件

    正软商城iOS软件专区,精选适用于iPhone和iPad的办公、学习、影音、设计、效率及AI应用,提供功能介绍、适用设备、系统要求和正版获取方式等信息。

    AI

    正软商城AI软件专区,汇集AI写作、AI绘画、AI视频、AI办公、AI编程、AI翻译、智能客服和数据分析等人工智能工具,提供功能介绍、适用平台、收费方式及正版购买信息。

    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    Mac软件 更多
    灵活计算器
    灵活计算器

    灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

    赤友清理大师
    赤友清理大师

    赤友清理大师是一款为 Mac 设计的智能清理优化工具,可精准扫描垃圾、大文件、重复文件等,释放磁盘空间。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

    极度公式
    极度公式

    极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

    图几
    图几

    图几是一款适用于 macOS 的截图、标注与美化工具,支持离线操作保障隐私。界面整理和高频系统操作被放到一起考虑,桌面或窗口内容一多时,管理起来会更省心。

    密码键盘
    密码键盘

    密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。

    思源笔记
    思源笔记

    思源笔记是一款本地笔记软件,提供所见即所得的编辑方式,为长文写作带来顺滑的体验。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

    Office 365 简体中文
    Office 365 简体中文

    一款文字处理软件,一种订阅式的跨平台办公软件,基于云平台提供多种服务,通过将 Excel 和 Outlook 等应用与 OneDrive 和 Microsoft Teams 等强大的云服务相结合,Office 365 可让任何人使用任何设备随时随地创建和共享内容。

    WALTR PRO
    WALTR PRO

    WALTR是一款电脑至iOS文件传输转换工具,操作简单,快速实现文件识别与传送。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

    CodeExpander
    CodeExpander

    CodeExpander 是一款快捷短语输入增强工具,通过键入缩写自动展开为自定义文段,提升工作效率。任务管理和过程控制会更完整,持续下载、批量同步或需要稳定传输流程的场景会更适合它。

    Mountain Duck
    Mountain Duck

    Mountain Duck 是一款能将多个网盘挂载到本地的工具,像本地磁盘一样使用网盘。清理链路的完整性会更好一些,做应用卸载、残留处理和空间整理时,通常能少走很多手动排查步骤。

    Menuist
    Menuist

    Menuist 是一款面向 macOS 的 Finder 右键菜单增强工具,主要用来补充新建文件、快捷导航等常用操作,让日常文件管理和访问路径时更高效、更顺手。

    Mole
    Mole

    Mole 是一款专为 Mac 设计的深度清理优化工具,涵盖缓存清理、应用管理及实时状态监控等功能。清理链路的完整性会更好一些,做应用卸载、残留处理和空间整理时,通常能少走很多手动排查步骤。

    WINDOWS 更多
    Windows 10
    Windows 10

    Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

    极度公式
    极度公式

    极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

    密码键盘
    密码键盘

    密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。

    思源笔记
    思源笔记

    思源笔记是一款本地笔记软件,提供所见即所得的编辑方式,为长文写作带来顺滑的体验。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

    傲梅轻松备份
    傲梅轻松备份

    傲梅轻松备份是一款专业易用的数据备份软件,为重要数据提供安全保障。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。

    Office 365 简体中文
    Office 365 简体中文

    一款文字处理软件,一种订阅式的跨平台办公软件,基于云平台提供多种服务,通过将 Excel 和 Outlook 等应用与 OneDrive 和 Microsoft Teams 等强大的云服务相结合,Office 365 可让任何人使用任何设备随时随地创建和共享内容。

    Wise Folder Hider Pro
    Wise Folder Hider Pro

    Wise Folder Hider Pro 是一款专业级文件和文件夹隐藏加密软件,为私密数据添加多重保护。高频操作更强调就近处理,浏览、整理和跨目录移动文件时,来回切换和重复点击都会少很多。

    WALTR PRO
    WALTR PRO

    WALTR是一款电脑至iOS文件传输转换工具,操作简单,快速实现文件识别与传送。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

    CodeExpander
    CodeExpander

    CodeExpander 是一款快捷短语输入增强工具,通过键入缩写自动展开为自定义文段,提升工作效率。任务管理和过程控制会更完整,持续下载、批量同步或需要稳定传输流程的场景会更适合它。

    PinStack
    PinStack

    PinStack是一款轻量级的Windows平台剪贴板管理工具,优化您的剪贴板使用体验。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

    Mountain Duck
    Mountain Duck

    Mountain Duck 是一款能将多个网盘挂载到本地的工具,像本地磁盘一样使用网盘。清理链路的完整性会更好一些,做应用卸载、残留处理和空间整理时,通常能少走很多手动排查步骤。

    Seer
    Seer

    Seer是一款在Win平台下的空格键功能增强效率工具,只需轻敲空格键,就能预览几乎任何格式的文件。它更适合把零散的小功能集中起来使用,处理高频琐碎任务时会更省事。