Deprecated: Creation of dynamic property Typecho\Widget\Request::$feed is deprecated in /www/wwwroot/blog.iletter.top/var/Widget/Archive.php on line 253
白荼日记 - java 2025-07-04T20:26:41+08:00 Typecho https://blog.iletter.top/index.php/feed/atom/tag/java/ <![CDATA[建筑类网站爬虫]]> https://blog.iletter.top/index.php/archives/364.html 2025-07-04T20:26:41+08:00 2025-07-04T20:26:41+08:00 DelLevin https://blog.iletter.top 最近帮我同学写相关建筑类网站的爬虫以及前后端搜索界面功能,其实技术要点一个没有,涉及到加密的网站我也是放弃爬虫,解密太麻烦了。

简单的网站都是一套的逻辑爬虫,大家可以参考一下。有兴趣的话帮忙点个start支持一下

前后端系统以及数据库

https://gitee.com/wonder19991209/mohurd\_search\_sys

爬虫脚本

https://gitee.com/wonder19991209/mohurd-spider

]]>
<![CDATA[Centos7配置java环境]]> https://blog.iletter.top/index.php/archives/345.html 2024-12-27T10:59:30+08:00 2024-12-27T10:59:30+08:00 DelLevin https://blog.iletter.top 下载:http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html

解压文件

tar -zxvf jdk-8u431-linux-x64.tar.gz -C /home/env/java/

设置环境变量

vim /etc/profile

在文件最末尾加上

export JAVA_HOME=/home/env/java/jdk1.8.0_431
export JRE_HOME=${JAVA_HOME}/jre
export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
export PATH=${JAVA_HOME}/bin:$PATH

使环境变量生效

source /etc/profile

配置软连接

ln -s /home/env/java/jdk1.8.0_431/bin/java /usr/bin/java

检查

java -version

删除软连接并重新配置

确认现有软连接

ls -l /usr/bin/java

删除

rm /usr/bin/java

重新配置

ln -s /env/java/jdk-21.0.3/bin/java /usr/bin/java
]]>
<![CDATA[java项目引入resources下面的文件]]> https://blog.iletter.top/index.php/archives/173.html 2024-09-26T23:57:00+08:00 2024-09-26T23:57:00+08:00 DelLevin https://blog.iletter.top 好久没写java,差点都忘了。。。

起因是一个资源文件在resources目录下面,转换后的地址类型是string类型不是file,于是用类加载器获取文件资源路径

String dbPath = GetIpAddr.class.getClassLoader().getResource("ip2region/ip2region.xdb").getPath();

但是这样在编译部署完毕后会报错

Generating unique operation named: userInfoUsingGET_1
2024-09-26 17:15:40.345  INFO 3992 --- [nio-8081-exec-1] o.a.c.c.C.[.[localhost].[/sys_api]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
failed to load vector index from file:/C:/Users/Administrator/Desktop/jar/api.jar!/BOOT-INF/classes!/ip2region/ip2region.xdb: java.io.FileNotFoundException: file:\C:\Users\Administrator\Desktop\jar\api.jar!\BOOT-INF\classes!\ip2region\ip2region.xdb (文件名、目录名或卷标语法不正确。)
2024-09-26 17:15:40.664 ERROR 3992 --- [nio-8081-exec-1] i.d.exception.RenExceptionHandler        :
### Error updating database.  Cause: com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Data too long for column 'ip_addr' at row 1
### The error may exist in URL [jar:file:/C:/Users/Administrator/Desktop/jar/api.jar!/BOOT-INF/classes!/mapper/MarkVisiter.xml]
### The error may involve io.dellevin.dao.MarkVisiterDao.insertVisitRecord-Inline
### The error occurred while setting parameters
### SQL: INSERT INTO mark_visiter             ( visit_url, ip, ip_addr, create_time)         VALUES             (                  ?, ?, ?, NOW()             )
### Cause: com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Data too long for column 'ip_addr' at row 1
; Data truncation: Data too long for column 'ip_addr' at row 1; nested exception is com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Data too long for column 'ip_addr' at row 1

org.springframework.dao.DataIntegrityViolationException:

只能用流方式加载文件,于是采用如下方式:可以使用一个临时文件来处理这个问题。首先读取输入流,然后将其写入一个临时文件,最后将该临时文件的路径作为 dbPath

String dbPath = null;

        // 1、从 dbPath 中预先加载 VectorIndex 缓存
        try (InputStream inputStream = GetIpAddr.class.getClassLoader().getResourceAsStream("ip2region/ip2region.xdb")) {
            if (inputStream == null) {
                throw new FileNotFoundException("Resource not found: ip2region/ip2region.xdb");
            }

            // 创建临时文件
            File tempFile = Files.createTempFile("ip2region", ".xdb").toFile();
            try (FileOutputStream outputStream = new FileOutputStream(tempFile)) {
                byte[] buffer = new byte[1024];
                int bytesRead;
                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, bytesRead);
                }
            }

            // 获取临时文件路径
            dbPath = tempFile.getAbsolutePath();
        } catch (IOException e) {
            System.out.printf("failed to load vector index: %s\n", e);
            return "failed to load vector index: " + e + " ";
        }
]]>
<![CDATA[简单优化-针对大数据查询的一些小思路]]> https://blog.iletter.top/index.php/archives/156.html 2024-04-10T23:50:00+08:00 2024-04-10T23:50:00+08:00 DelLevin https://blog.iletter.top 我的member_user里面有352599条数据,gp_project里面有1211974条数据,请问该如何优化这个sql的查询效率

SELECT gp.*, mu.linkmanName , mu.linkmanPhone, mu.legalPersonName, mu.legalPersonPhone, mu.address, mu.registerArea FROM gp_project gp LEFT JOIN member_user mu ON mu.supplierId = gp.supplier_id WHERE DATE(gp.publicity_time) >= '2024-04-03' AND DATE(gp.publicity_time) <= '2024-04-07' and mu.deleted = 0

针对这样的数据,我的数据表里面数据太多,导致了查询需要512秒的时间,而且这仅仅是两个表的关联,之后还需要关联四五个表组成一条查询语句,如果仅在mysql里面查,未免有些太为难服务器了。因为平常找数据用的sql里面的in语法比较多,所以尝试了一下用关联字段in数据的情况,突然发现,这样还挺快,主要是因为关联字段是索引,所以比较快,通过单表查出数据,然后将关键字段提取出来,in到另一个表里面快速查询,最后拼接字段,来实现数据的查询结果。

另外因为涉及到多个循环,如果仅仅遵循 循环的外小内大原则,这样未免要写多个循环,降低了程序的运行效率多个O(n^2)的程序跑的,这样未免太致命了。改用map的key和value对照关系这样通过一个循环+map的方式降低时间复杂度为O(1) ,这样就会快很多

下面是操作案例

mybaits部分(单个案例)


<?xml version="1.0" encoding="UTF-8"?>  
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">  
  
<mapper namespace="com.xyjq.mapper.db1.LoginMemberUserDao">  
  
    <!--根据供应商id判断是否为注册用户 -->  
    <select id="isRegisterSupplier" resultType="com.xyjq.entity.db1.LoginMemberUserEntity">  
        SELECT supplierId,createTime FROM `login_member_user` WHERE supplierId in  
        <foreach collection="supplierIdList" item="supplierId" open="(" separator="," close=")">  
            #{supplierId}  
        </foreach>  
    </select>  
  
</mapper>

业务层部分

List<GpProjectEntity> list = baseDao.queryList(params);  
  
// 提取查询结果里面的supplierID的set集合  
Set<Integer> supplierIdList =  list.stream()  
                            .map(GpProjectEntity::getSupplierId)  
                            .filter(Objects::nonNull)  
                            .collect(Collectors.toSet());  
SimpleDateFormat inputFormat = new SimpleDateFormat("yyyyMMddHHmmss");  
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
// 以下遍历中防止嵌套for循环O(n^2),使用map对象存储降低时间复杂度为O(1)  
// 优化设置供应商信息  
Map<String, MemberUserEntity> memberUserInfoMap = new HashMap<>();  
memberUserDao.getMemberUserInfo(supplierIdList).forEach(memberUser -> memberUserInfoMap.put(memberUser.getSupplierId().toString(), memberUser));  
// 设置供应商的工商地址信息  
Map<String, SupplierBussinessInformationEntity> supplierBusinessInfoMap = new HashMap<>();  
supplierBussinessInformationDao.getSupplierBusinessInfo(supplierIdList).forEach(supplierBusinessInfo -> supplierBusinessInfoMap.put(supplierBusinessInfo.getSupplierId().toString(), supplierBusinessInfo));  
// 设置是否是注册用户  
Map<String, LoginMemberUserEntity> loginMemberUserMap = new HashMap<>();  
loginMemberUserDao.isRegisterSupplier(supplierIdList).forEach(loginMemberUser -> loginMemberUserMap.put(loginMemberUser.getSupplierId().toString(), loginMemberUser));  
// 设置是否有融资需求  
Map<String, String> financingNeedsMap = new HashMap<>();  
// 根据搜索结果,如果key一样就拼接value  
financingNeedsDao.getCompFinancingNeeds(supplierIdList).forEach(financingNeeds -> {  
    String key = financingNeeds.getSupplierId();  
    String value = FinancingStatus.getDescriptionByCode(financingNeeds.getFinancingProgress()) + "(" + financingNeeds.getCreateTime() + ")";  
    financingNeedsMap.merge(key, value, (oldValue, newValue) -> oldValue + ", " + newValue);  
});  
  
for (GpProjectEntity project : list) {  
    // 设置项目名称  
    if (project.getAgName() != null && !project.getAgName().isEmpty()) {  
        project.setProjectNameMergeAgBid(project.getAgName());  
    } else if (project.getJatTpProName() != null && !project.getJatTpProName().isEmpty()) {  
        project.setProjectNameMergeAgBid(project.getJatTpProName());  
    }  
    // 设置项目金额  
    if (project.getAgTotalMoney() != null) {  
        project.setProjectMoneyMergeAgBid(project.getAgTotalMoney());  
    } else {  
        project.setProjectMoneyMergeAgBid(project.getWinPriceRevised());  
    }  
    // 设置项目的日期  
    if (project.getAgSignDate() != null) {  
        Date date = null;  
        try {  
            date = inputFormat.parse(project.getAgSignDate());  
        } catch (ParseException e) {  
            throw new RuntimeException(e);  
        }  
        project.setProjectDateMergeAgBid(dateFormat.format(date));  
    } else {  
        project.setProjectDateMergeAgBid( dateFormat.format(project.getJatWinTime()) );  
    }  
  
    String supplierId = project.getSupplierId().toString();  
    // 设置供应商信息  
    MemberUserEntity memberUser = memberUserInfoMap.get(supplierId);  
    if (memberUser != null) {  
        project.setProjectLinkManName(memberUser.getLinkmanName());  
        project.setProjectLinkManPhone(memberUser.getLinkmanPhone());  
        project.setProjectLegalPersonName(memberUser.getLegalPersonName());  
        project.setProjectLegalPersonPhone(memberUser.getLegaPersonPhone());  
        project.setProjectAddress(memberUser.getAddress());  
        project.setProjectRegisterArea(memberUser.getRegisterArea());  
    }  
    // 设置工商信息省市区  
    SupplierBussinessInformationEntity supplierBussinessInformation = supplierBusinessInfoMap.get(supplierId);  
    if (supplierBussinessInformation != null) {  
        project.setProjectProvince(supplierBussinessInformation.getProvince());  
        project.setProjectCity(supplierBussinessInformation.getCity());  
        project.setProjectDistrict(supplierBussinessInformation.getDistrict());  
    }  
    // 设置是否是注册用户  
    LoginMemberUserEntity loginMemberUser = loginMemberUserMap.get(supplierId);  
    if (loginMemberUser != null) {  
        project.setProjectIsRegisterSupplier("已注册(" + dateFormat.format(loginMemberUser.getCreateTime()) + ")");  
    }  
    // 设置融资历史记录  
    String financingNeeds = financingNeedsMap.get(supplierId);  
    if (financingNeeds != null) {  
        project.setProjectFinancingNeeds(financingNeeds);  
    }  
}
]]>
<![CDATA[判断两个事件是否存在冲突]]> https://blog.iletter.top/index.php/archives/132.html 2023-05-17T23:48:00+08:00 2023-05-17T23:48:00+08:00 DelLevin https://blog.iletter.top 给你两个字符串数组 event1event2 ,表示发生在同一天的两个闭区间时间段事件,其中:

  • event1 = [startTime1, endTime1]
  • event2 = [startTime2, endTime2]

事件的时间为有效的 24 小时制且按 HH:MM 格式给出。

当两个事件存在某个非空的交集时(即,某些时刻是两个事件都包含的),则认为出现 冲突

如果两个事件之间存在冲突,返回 true ;否则,返回 false

示例 1:

输入:event1 = ["01:15","02:00"], event2 = ["02:00","03:00"]
输出:true
解释:两个事件在 2:00 出现交集。

示例 2:

输入:event1 = ["01:00","02:00"], event2 = ["01:20","03:00"]
输出:true
解释:两个事件的交集从 01:20 开始,到 02:00 结束。

示例 3:

输入:event1 = ["10:00","11:00"], event2 = ["14:00","15:00"]
输出:false
解释:两个事件不存在交集。

提示:

  • evnet1.length == event2.length == 2.
  • event1[i].length == event2[i].length == 5
  • startTime1 <= endTime1
  • startTime2 <= endTime2
  • 所有事件的时间都按照 HH:MM 格式给出

代码

class Solution {
    public boolean haveConflict(String[] event1, String[] event2) {
        int event1StartTime = strToIntTime(event1[0]);
        int event1EndTime   = strToIntTime(event1[1]);
        int event2StartTime = strToIntTime(event2[0]);
        int event2EndTime   = strToIntTime(event2[1]);

        if(event1EndTime < event2StartTime || event2EndTime <event1StartTime ){
            return false;
        }
        return true;
    }
    public static int strToIntTime(String str) {
        String[] ss = str.split(":");
        int hours = Integer.parseInt(ss[0])*60;
        return  hours+Integer.parseInt(ss[1]);
    }
}

思路

比对时间不在另一个区间内就可以了,但是string不能进行比较,只能使用int类型比较。

]]>
<![CDATA[负二进制数相加]]> https://blog.iletter.top/index.php/archives/145.html 2023-05-07T23:48:00+08:00 2023-05-07T23:48:00+08:00 DelLevin https://blog.iletter.top 给出基数为 -2 的两个数 arr1arr2,返回两数相加的结果。

数字以 数组形式 给出:数组由若干 0 和 1 组成,按最高有效位到最低有效位的顺序排列。例如,arr = [1,1,0,1] 表示数字 (-2)^3 + (-2)^2 + (-2)^0 = -3数组形式 中的数字 arr 也同样不含前导零:即 arr == [0]arr[0] == 1

返回相同表示形式的 arr1arr2 相加的结果。两数的表示形式为:不含前导零、由若干 0 和 1 组成的数组。

示例 1:

输入:arr1 = [1,1,1,1,1], arr2 = [1,0,1]
输出:[1,0,0,0,0]
解释:arr1 表示 11,arr2 表示 5,输出表示 16 。

示例 2:

输入:arr1 = [0], arr2 = [0]
输出:[0]

示例 3:

输入:arr1 = [0], arr2 = [1]
输出:[1]

提示:

  • 1 <= arr1.length, arr2.length <= 1000
  • arr1[i]arr2[i] 都是 01
  • arr1arr2 都没有前导0

代码:

class Solution {
    public int[] addNegabinary(int[] arr1, int[] arr2) {
        // 定义规则:0+1=1,进位向前边减一,够减则减,不够减则借位
        int[] outCome = null;
        if (arr1.length > arr2.length) {
            for (int i = 1; i <= arr2.length; i++) {
                arr1[arr1.length - i] += arr2[arr2.length - i];
            }
            outCome = arr1;
        } else {
            for (int i = 1; i <= arr1.length; i++) {
                arr2[arr2.length - i] += arr1[arr1.length - i];
            }
            outCome = arr2;
        }
        // System.out.println(Arrays.toString(outCome));
        int add = 0;
        for (int i = outCome.length - 1; i > 0; i--) {
            if (outCome[i] + add >= 2) {
                add = -1;
                outCome[i] -= 2;
            } else if (outCome[i] + add < 0) {
                outCome[i - 1] += 1;
                outCome[i] = 1;
                add = 0;
            } else {
                outCome[i] += add;
                add = 0;
            }
        }
        outCome[0] += add;
        //System.out.println(Arrays.toString(outCome));
        if (outCome[0] >= 2) {
            outCome[0] -= 2;
            int[] newOutCome = new int[outCome.length + 2];
            newOutCome[0] = 1;
            newOutCome[1] = 1;
            for (int i = 0; i < outCome.length; i++) {
                newOutCome[2 + i] = outCome[i];
            }
            outCome = newOutCome;
        }
        // System.out.println(Arrays.toString(outCome));
        if (outCome[0] == 0) {
            int zlength = 0;
            while (zlength < outCome.length && outCome[zlength] == 0) {
                zlength++;
            }
            System.out.println(zlength);
            if (zlength == outCome.length) {
                outCome = new int[1];
                outCome[0] = 0;
            } else {
                int[] newOutCome = new int[outCome.length - zlength];
                for (int i = 0; i < newOutCome.length; i++) {
                    newOutCome[i] = outCome[zlength + i];
                }
                outCome = newOutCome;
            }
        }

        return outCome;
    }
}

错误的办法:

这里主要错误的原因是因为忽略了数组之间的长度,因为长度是1000也就是最大的数是-2的1000次幂。远远的超出了bigint,int,以及long类型的计算长度。所以下面的办法是妥妥的不对的

关键字所占位数范围
int32−231−231 ~ 2 ^{31} - 1
long64−263−263 ~ 2 ^{63} - 1
class Solution {
    public int[] addNegabinary(int[] arr1, int[] arr2) {
        int xx = lowBinaryToInt(arr1) + lowBinaryToInt(arr2);
        String ss= intToLowBinaryString(xx);
        String[] ssArr = ss.split("");
        int[] res = new int[ss.length()];
        for (int i = 0; i <= ss.length()-1; i++) {
            res[i] = Integer.parseInt(ssArr[i]);
        }
        return res;
    }
    public static int lowBinaryToInt(int[] arr) {
        double res=0;
        int xx = 0;
        for (int i = arr.length - 1; i>=0; i--) {
            if (arr[i] == 1) {
                res = res+Math.pow(-2, xx);
                xx++;
            }else{
                xx++;
            }
        }
        return (int)res;
    }
    public static String intToLowBinaryString(int N) {
        StringBuilder sb = new StringBuilder();
        while (N != 0) {
            int r = N % -2;
            N = N / -2;
            if (r < 0) {
                r = r + 2;
                N = N+1;
            }
            sb.append(r);
        }
        return sb.length() > 0 ? sb.reverse().toString() : "0";
    }
}
]]>
<![CDATA[vue+springboot实现预览word]]> https://blog.iletter.top/index.php/archives/112.html 2023-04-29T23:48:00+08:00 2023-04-29T23:48:00+08:00 DelLevin https://blog.iletter.top 其实这个有很多办法可以实现。

利用office的在线预览,vue的依赖,转换成pdf进行预览。三个办法我都试了。微软官方的office在线预览的话需要一个公网ip,同时有的时候还需要你科学上网,这个直接寄。

word官方预览链接(支持三种格式,部分word带特殊符号或流程图无法显示)

      const routeUrl = file.url; // 文件路径
      const url = encodeURIComponent(routeUrl);
      const officeUrl = "https://view.xdocin.com/view?src=" + url;

office官方预览(pdf不能展示)

let officeUrl = 'http://view.officeapps.live.com/op/view.aspx?src='+url

vue的依赖的话,GitHub上搜索一下会有很多。vue-office这个项目很棒,可惜我引入包和标签的时候会报错,人家用的是js,而我用的是ts,这个也pass了。这个是项目地址,非常棒的一个项目,可以参考学习一下:https://github.com/501351981/vue-office

到最后,还要回到word转换成office的破办法,这是我最难接受的一个,用的人一多,后端压力就大了,万一流量上来了,服务器直接开摆,笑死。但是实在是没办法了,退退退而求其次,只能这样了。主要思路是前端把文件id获取到,去数据库搜索到存放word的url地址,后端通过url地址把doc文档转换成输入流,然后通过输出流存放到一个专属的pdf文件夹,再把这个pdf生成链接给前端,前端显示的话就用这一个链接进行显示。

前端代码

<template>
  <el-dialog v-model="visible" :title="$t('文件预览')" :close-on-click-modal="false" :close-on-press-escape="false">
    <div v-if="fileMessage.fileType == 'jpg' ||
      fileMessage.fileType == 'png' ||
      fileMessage.fileType == 'ico' ||
      fileMessage.fileType == 'gif' ||
      fileMessage.fileType == 'webp' 
      ">
      <el-image style="width: 100%" :src="fileMessage.getFilePath"> </el-image>
    </div>
    <div v-else-if="fileMessage.fileType == 'docx' || fileMessage.fileType == 'doc'">
      <embed :src="result.url" style="width: 100%; height: 600px" />
      <!-- <h2 style="text-align: center;color: brown; ">暂不支持doc、docx格式!</h2> -->
    </div>
    <div v-else-if="fileMessage.fileType == 'xlsx' || fileMessage.fileType == 'xls'">
      <h2 style="text-align: center;color: brown; ">暂不支持该xlsx、xls格式!</h2>
    </div>
    <div v-else-if="fileMessage.fileType == 'pdf'">
      <embed :src="fileMessage.getFilePath" style="width: 100%; height: 600px" />
    </div>
    <div v-else>
      <h2 style="text-align: center;color: brown; ">暂不支持该文件格式!</h2>
    </div>
    <template v-slot:footer>
      <el-button @click="visible = false">{{ $t("退出预览") }}</el-button>
    </template>
  </el-dialog>
</template>
<script lang="ts" setup>
import { reactive, ref } from "vue";
import baseService from "@/service/baseService";
import { ElMessage } from "element-plus";

const visible = ref(false);
const dataFormRef = ref();

const fileMessage = reactive({
  getFilePath: "",
  fileType: ""
});

const dataForm = reactive({
  uploadFileId: "",
  uploadCompanyName: "",
  uploadFileName: "",
  uploadFilePath: "",
  uploadUserName: "",
  uploadFileTime: "",
});

const init = (uploadFileId?: number) => {
  visible.value = true;
  dataForm.uploadFileId = "";

  //重置表单数据
  if (dataFormRef.value) {
    dataFormRef.value.resetFields();
  }

  if (uploadFileId) {
    getInfo(uploadFileId);
  }
};

const result = reactive({
  url: "",
  msg: ""
});
//var showType: string;
// 获取信息
const getInfo = (uploadFileId: number) => {
  baseService.get("/uploadFile/xyjqfileupload/" + uploadFileId).then((res) => {
    Object.assign(dataForm, res.data);
    if (dataForm.uploadFilePath != "") {
      fileMessage.getFilePath = dataForm.uploadFilePath; //获取到图片的路径,写在这里面才管用,写在外面,第一次请求时候没有数据
      //console.log(getFilePath.substring(getFilePath.lastIndexOf(".") + 1, getFilePath.length));
      //console.log(getFilePath);
      fileMessage.fileType = fileMessage.getFilePath.substring(fileMessage.getFilePath.lastIndexOf(".") + 1, fileMessage.getFilePath.length);
      //showType = fileType;
      console.log("文件类型:" + fileMessage.fileType);
      console.log(fileMessage.getFilePath);
      if (fileMessage.fileType == "doc" || fileMessage.fileType == "docx") {
        baseService
          .post(`/uploadFile/xyjqfileupload/pdfUrl/${uploadFileId}`)
          .then((res) => {
            if (res.code === 0) {
              result.url = res.data;
              console.log(res.data);
            } else {
              ElMessage.error(res.msg);
              result.msg = res.msg;
            }
          })
          .catch((err) => {
            ElMessage.error(err.message);
          });
      }
    } else {
      ElMessage.error("链接地址为空,无法打开");
    }
  });
};

defineExpose({
  init
});
</script>

主要核心代码是这里

// 获取信息
const getInfo = (uploadFileId: number) => {
  baseService.get("/uploadFile/xyjqfileupload/" + uploadFileId).then((res) => {
    Object.assign(dataForm, res.data);
    if (dataForm.uploadFilePath != "") {
      fileMessage.getFilePath = dataForm.uploadFilePath; //获取到图片的路径,写在这里面才管用,写在外面,第一次请求时候没有数据
      //console.log(getFilePath.substring(getFilePath.lastIndexOf(".") + 1, getFilePath.length));
      //console.log(getFilePath);
      fileMessage.fileType = fileMessage.getFilePath.substring(fileMessage.getFilePath.lastIndexOf(".") + 1, fileMessage.getFilePath.length);
      //showType = fileType;
      console.log("文件类型:" + fileMessage.fileType);
      console.log(fileMessage.getFilePath);
      if (fileMessage.fileType == "doc" || fileMessage.fileType == "docx") {
        baseService
          .post(`/uploadFile/xyjqfileupload/pdfUrl/${uploadFileId}`)
          .then((res) => {
            if (res.code === 0) {
              result.url = res.data;
              console.log(res.data);
            } else {
              ElMessage.error(res.msg);
              result.msg = res.msg;
            }
          })
          .catch((err) => {
            ElMessage.error(err.message);
          });
      }
    } else {
      ElMessage.error("链接地址为空,无法打开");
    }
  });
};

主要就是获取到文件之后看看文件后缀,欸!是doc或者docx格式的,那就继续post后端,把文件id发过去,这样后端就能通过id在数据库里面找到文件的在线链接。然后进行下载解析word。

后端部分代码

    @PostMapping("/pdfUrl/{fileId}")
    @ApiOperation("WORD转换PdF")
    @LogOperation("WORD转换pdf")
    public Result  wordToPdf(@PathVariable("fileId") String fileId) throws Exception {
        XyjqFileUploadEntity xyjqFileUploadEntity = xyjqFileUploadService.selectById(fileId);
        //获取到url地址
        String url = xyjqFileUploadEntity.getUploadFilePath();
        //给新保存的文件起一个新的名字
        String strFileNameAndPoint = url.substring(url.lastIndexOf("/")+1);
        String newFileName =  strFileNameAndPoint.substring(0,strFileNameAndPoint.lastIndexOf("."))+"_pdf.pdf";
        System.out.println(newFileName);
        String paths = "D:\\Archive\\Desktop\\uploadFile\\WordToPdf\\"+newFileName;
        //先提前构建一个生成的pdf保存路径
        File outputFile = new File(paths);

        //先把获取到的文件转换成输入流的模式
        URL receiveUrl = new URL(url);
        HttpURLConnection conn = (HttpURLConnection)receiveUrl.openConnection();
        //设置超时间为3秒
        conn.setConnectTimeout(3*1000);
        //将传输过来的doc文件转换成流
        InputStream docxInputStream = conn.getInputStream();
        try  {
            OutputStream outputStream = new FileOutputStream(outputFile);
            IConverter converter = LocalConverter.builder().build();
            converter.
                    convert(docxInputStream).as(DocumentType.DOCX).//作为doc格式
                    to(outputStream).as(DocumentType.PDF).
                    execute();
            outputStream.close();
            System.out.println("Word To Pdf 转换完成");
        } catch (Exception e) {
            e.printStackTrace();
        }

        //将file格式转换成MultipartFile格式
        File file = new File(paths);
        MultipartFile cMultiFile = getMultipartFile(file);
        //获取到新的url返还给前端进行预览
        String newUrl = OSSFactory.build().uploadSuffix(cMultiFile.getBytes(), "pdf");
        System.out.println(newUrl);
        return new Result().ok(newUrl);
    }
    public static MultipartFile getMultipartFile(File file) {
        DiskFileItem item = new DiskFileItem("file"
                , MediaType.MULTIPART_FORM_DATA_VALUE
                , true
                , file.getName()
                , (int)file.length()
                , file.getParentFile());
        try {
            OutputStream os = item.getOutputStream();
            os.write(FileUtils.readFileToByteArray(file));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new CommonsMultipartFile(item);
    }

我开始是想传输url的,但是post之后,发现失败,原来前端不承认这样的链接啊,我也是太天真了,所以只能这么做了。

哦对,还需要引入两个依赖

        <!-- 转换成PDF-->
        <dependency>
            <groupId>com.documents4j</groupId>
            <artifactId>documents4j-local</artifactId>
            <version>1.0.3</version>
        </dependency>
        <dependency>
            <groupId>com.documents4j</groupId>
            <artifactId>documents4j-transformer-msoffice-word</artifactId>
            <version>1.0.3</version>
        </dependency>

基本情况是这个样子,中间还少了一些判断,比如第二次打开的时候,这个链接其实以及生成一个pdf了,再次预览的话,又要生成一个一样的就覆盖了,实在是太占用资源了,还不如去mysql里面做个表去保存这个生成的预览pdf链接,然后直接返还,这样就能再一定程度节省服务器资源了。目前没写主要是太困了,我需要睡觉了。。。想起来再写吧。

]]>
<![CDATA[Java截取字符]]> https://blog.iletter.top/index.php/archives/108.html 2023-04-26T23:48:00+08:00 2023-04-26T23:48:00+08:00 DelLevin https://blog.iletter.top 今天脑子突然抽了,截取字符的时候

想把

[1651114259328487426]

截取成

1651114259328487426

我知道是substring从1开始截取,但是为什么最后是length()-1呢,一开始-2我还很疑惑,看了一些文档才反应过来。

  • beginIndex -- 起始索引(包括), 索引从 0 开始。
  • endIndex -- 结束索引(不包括)。

原来是最后的索引是不包括的呀。。。自己真的蠢到家了,自己把自己蠢到了,说到底基础还是不扎实。

Java截取最后一个/后面的所有字符

String imgUrl = "http://127.0.0.1:8080/cms/ReadAddress/1479805098158.jpg";

String image = imgUrl.substring(imgUrl.lastIndexOf("/")+1);
]]>
<![CDATA[Failed to start bean documentationPluginsBootstrapper]]> https://blog.iletter.top/index.php/archives/84.html 2023-04-03T23:48:00+08:00 2023-04-03T23:48:00+08:00 DelLevin https://blog.iletter.top 关于报错org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'


Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2023-04-03 00:08:57.651 ERROR 15032 --- [           main] o.s.boot.SpringApplication               : Application run failed

org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException
    at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:181) ~[spring-context-5.3.22.jar:5.3.22]
    at org.springframework.context.support.DefaultLifecycleProcessor.access$200(DefaultLifecycleProcessor.java:54) ~[spring-context-5.3.22.jar:5.3.22]
    at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:356) ~[spring-context-5.3.22.jar:5.3.22]
    at java.base/java.lang.Iterable.forEach(Iterable.java:75) ~[na:na]
    at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:155) ~[spring-context-5.3.22.jar:5.3.22]
    at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:123) ~[spring-context-5.3.22.jar:5.3.22]
    at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:935) ~[spring-context-5.3.22.jar:5.3.22]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:586) ~[spring-context-5.3.22.jar:5.3.22]
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:145) ~[spring-boot-2.6.10.jar:2.6.10]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:745) ~[spring-boot-2.6.10.jar:2.6.10]
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:420) ~[spring-boot-2.6.10.jar:2.6.10]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:307) ~[spring-boot-2.6.10.jar:2.6.10]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1317) ~[spring-boot-2.6.10.jar:2.6.10]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306) ~[spring-boot-2.6.10.jar:2.6.10]
    at com.wonder.UselessToolsApplication.main(UselessToolsApplication.java:14) ~[classes/:na]
Caused by: java.lang.NullPointerException: null
    at springfox.documentation.spi.service.contexts.Orderings$8.compare(Orderings.java:112) ~[springfox-spi-2.9.2.jar:null]
    at springfox.documentation.spi.service.contexts.Orderings$8.compare(Orderings.java:109) ~[springfox-spi-2.9.2.jar:null]
    at com.google.common.collect.ComparatorOrdering.compare(ComparatorOrdering.java:37) ~[guava-20.0.jar:na]
    at java.base/java.util.TimSort.countRunAndMakeAscending(TimSort.java:355) ~[na:na]
    at java.base/java.util.TimSort.sort(TimSort.java:220) ~[na:na]
    at java.base/java.util.Arrays.sort(Arrays.java:1441) ~[na:na]
    at com.google.common.collect.Ordering.sortedCopy(Ordering.java:855) ~[guava-20.0.jar:na]
    at springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider.requestHandlers(WebMvcRequestHandlerProvider.java:57) ~[springfox-spring-web-2.9.2.jar:null]
    at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper$2.apply(DocumentationPluginsBootstrapper.java:138) ~[springfox-spring-web-2.9.2.jar:null]
    at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper$2.apply(DocumentationPluginsBootstrapper.java:135) ~[springfox-spring-web-2.9.2.jar:null]
    at com.google.common.collect.Iterators$7.transform(Iterators.java:750) ~[guava-20.0.jar:na]
    at com.google.common.collect.TransformedIterator.next(TransformedIterator.java:47) ~[guava-20.0.jar:na]
    at com.google.common.collect.TransformedIterator.next(TransformedIterator.java:47) ~[guava-20.0.jar:na]
    at com.google.common.collect.MultitransformedIterator.hasNext(MultitransformedIterator.java:52) ~[guava-20.0.jar:na]
    at com.google.common.collect.MultitransformedIterator.hasNext(MultitransformedIterator.java:50) ~[guava-20.0.jar:na]
    at com.google.common.collect.ImmutableList.copyOf(ImmutableList.java:249) ~[guava-20.0.jar:na]
    at com.google.common.collect.ImmutableList.copyOf(ImmutableList.java:209) ~[guava-20.0.jar:na]
    at com.google.common.collect.FluentIterable.toList(FluentIterable.java:614) ~[guava-20.0.jar:na]
    at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.defaultContextBuilder(DocumentationPluginsBootstrapper.java:111) ~[springfox-spring-web-2.9.2.jar:null]
    at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.buildContext(DocumentationPluginsBootstrapper.java:96) ~[springfox-spring-web-2.9.2.jar:null]
    at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.start(DocumentationPluginsBootstrapper.java:167) ~[springfox-spring-web-2.9.2.jar:null]
    at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:178) ~[spring-context-5.3.22.jar:5.3.22]
    ... 14 common frames omitted

问题分析

因为Springfox 使用的路径匹配是基于AntPathMatcher的,而Spring Boot 2.6.X使用的是PathPatternMatcher。

修复方案

修复方案一:降低Spring Boot 版本到2.6.x以下版本

比如下面版本组合是兼容的

Spring Boot版本Swagger 版本
2.5.62.9.2

修复方案二: SpringBoot版本不降级解决方案

比如下面版本组合是兼容的

Spring Boot版本Swagger 版本
2.6.53.0.0

修复方案三:设置application配置文件

spring:
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher

修复方案四,修改spring源文件

我没找到这个java文件。。。。没试过,估计应该可以。。

参考地址:https://github.com/springfox/springfox/issues/3462

  1. revert matching strategy spring.mvc.pathmatch.matching-strategy to ant-path-matcher
  2. hack springfox WebMvcRequestHandlerProvider to filter out actuator controllers which don't respect spring.mvc.pathmatch.matching-strategy

        public WebMvcRequestHandlerProvider(Optional<ServletContext> servletContext, HandlerMethodResolver methodResolver,
                List<RequestMappingInfoHandlerMapping> handlerMappings) {
            this.handlerMappings = handlerMappings.stream().filter(mapping -> mapping.getPatternParser() == null)
                    .collect(Collectors.toList());

swagger配置类

.apis(RequestHandlerSelectors.basePackage("com.wonder.controller"))

这里填写扫描包,对应类添加@API

package com.wonder.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;


import javax.servlet.ServletContext;
import java.util.Optional;

@Configuration // 标明是配置类
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)  // DocumentationType.SWAGGER_2 固定的,代表swagger2
//                .groupName("分布式任务系统") // 如果配置多个文档的时候,那么需要配置groupName来分组标识
                .apiInfo(apiInfo()) // 用于生成API信息
                .select() // select()函数返回一个ApiSelectorBuilder实例,用来控制接口被swagger做成文档
                .apis(RequestHandlerSelectors.basePackage("com.wonder.controller")) // 用于指定扫描哪个包下的接口
                .paths(PathSelectors.any())// 选择所有的API,如果你想只为部分API生成文档,可以配置这里
                .build();
    }

    /**
     * 用于定义API主界面的信息,比如可以声明所有的API的总标题、描述、版本
     * @return
     */
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("项目API") //  可以用来自定义API的主标题
                .description("项目SwaggerAPI管理") // 可以用来描述整体的API
                //.termsOfServiceUrl("") // 用于定义服务的域名
                .version("1.0") // 可以用来定义版本。
                .build(); //
    }

}

访问地址:http://localhost:8080/swagger-ui.html

]]>
<![CDATA[java获取客户端的ip地址以及获取位置]]> https://blog.iletter.top/index.php/archives/82.html 2023-04-02T23:48:00+08:00 2023-04-02T23:48:00+08:00 DelLevin https://blog.iletter.top 主要依赖:

        <!--通过ip查询客户端位置-->
        <dependency>
            <groupId>org.lionsoul</groupId>
            <artifactId>ip2region</artifactId>
            <version>2.7.0</version>
        </dependency>
        <!-- Alibaba Fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>

首先先获取到访问者的ip地址

访问必须是通过公网访问,要是内网互ip访问相测试的话,会返回127.0.0.1,或者访问者的内网ip地址。

通过X-FORWARDED-FOR等信息。跟踪原有的客户端IP地址和原来客户端请求的服务器地址。

package com.wonder.utils;


import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;

public class getIPutils {
    public static String resClientIP(HttpServletRequest request) {
        String ip = getClientIpAddress(request);
        boolean  isInner = isInnerIP(ip);
        if (!isInner) {
            return  ip;
        }else {
            return  "内网ip";
        }

    }
    public static String getClientIpAddress(HttpServletRequest request) {
        // 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址
        String headerName = "x-forwarded-for";
        String ip = request.getHeader(headerName);
        if (null != ip && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
            // 多次反向代理后会有多个IP值,第一个IP才是真实IP,它们按照英文逗号','分割
            if (ip.indexOf(",") != -1) {
                ip = ip.split(",")[0];
            }
        }
        if (checkIp(ip)) {
            headerName = "Proxy-Client-IP";
            ip = request.getHeader(headerName);
        }
        if (checkIp(ip)) {
            headerName = "WL-Proxy-Client-IP";
            ip = request.getHeader(headerName);
        }
        if (checkIp(ip)) {
            headerName = "HTTP_CLIENT_IP";
            ip = request.getHeader(headerName);
        }
        if (checkIp(ip)) {
            headerName = "HTTP_X_FORWARDED_FOR";
            ip = request.getHeader(headerName);
        }
        if (checkIp(ip)) {
            headerName = "X-Real-IP";
            ip = request.getHeader(headerName);
        }
        if (checkIp(ip)) {
            headerName = "remote addr";
            ip = request.getRemoteAddr();
            // 127.0.0.1 ipv4, 0:0:0:0:0:0:0:1 ipv6
            if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) {
                //根据网卡取本机配置的IP
                InetAddress inet = null;
                try {
                    inet = InetAddress.getLocalHost();
                } catch (UnknownHostException e) {
                    e.printStackTrace();
                }
                ip = inet.getHostAddress();
            }
        }
        System.out.println("getClientIp  IP is " + ip + ", headerName = " + headerName);
        return ip;
    }
    private static boolean checkIp(String ip) {
        if (null == ip || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            return true;
        }
        return false;
    }
    /**
     * 判断IP是否是内网地址
     * @param ipAddress ip地址
     * @return 是否是内网地址
     */
    public static boolean isInnerIP(String ipAddress) {
        boolean isInnerIp;
        long ipNum = getIpNum(ipAddress);
        /**
         私有IP:A类  10.0.0.0-10.255.255.255
         B类  172.16.0.0-172.31.255.255
         C类  192.168.0.0-192.168.255.255
         还有127这个网段是环回地址
         **/
        long aBegin = getIpNum("10.0.0.0");
        long aEnd = getIpNum("10.255.255.255");

        long bBegin = getIpNum("172.16.0.0");
        long bEnd = getIpNum("172.31.255.255");

        long cBegin = getIpNum("192.168.0.0");
        long cEnd = getIpNum("192.168.255.255");
        isInnerIp = isInner(ipNum, aBegin, aEnd) || isInner(ipNum, bBegin, bEnd) || isInner(ipNum, cBegin, cEnd)
                || ipAddress.equals("127.0.0.1");
        return isInnerIp;
    }

    private static long getIpNum(String ipAddress) {
        String[] ip = ipAddress.split("\\.");
        long a = Integer.parseInt(ip[0]);
        long b = Integer.parseInt(ip[1]);
        long c = Integer.parseInt(ip[2]);
        long d = Integer.parseInt(ip[3]);

        return a * 256 * 256 * 256 + b * 256 * 256 + c * 256 + d;
    }

    private static boolean isInner(long userIp, long begin, long end) {
        return (userIp >= begin) && (userIp <= end);
    }

    public static String getRealIP(HttpServletRequest request){
        // 获取客户端ip地址
        String clientIp = request.getHeader("x-forwarded-for");

        if (clientIp == null || clientIp.length() == 0 || "unknown".equalsIgnoreCase(clientIp)) {
            clientIp = request.getRemoteAddr();
        }

        String[] clientIps = clientIp.split(",");
        if(clientIps.length <= 1) return clientIp.trim();

        // 判断是否来自CDN
        if(isComefromCDN(request)){
            if(clientIps.length>=2) return clientIps[clientIps.length-2].trim();
        }

        return clientIps[clientIps.length-1].trim();
    }

    private static boolean isComefromCDN(HttpServletRequest request) {
        String host = request.getHeader("host");
        return host.contains("www.189.cn") ||host.contains("shouji.189.cn") || host.contains(
                "image2.chinatelecom-ec.com") || host.contains(
                "image1.chinatelecom-ec.com");
    }

}

相关请求头

  • X-Forwarded-For 记录一个请求从客户端出发到目标服务器过程中经历的代理,或者负载平衡设备的IP。这是由缓存代理软件 Squid 引入,用来表示 HTTP 请求端真实 IP,现在已经成为事实上的标准,被各大 HTTP 代理、负载均衡等转发服务广泛使用,并被写入 RFC 7239(Forwarded HTTP Extension)标准之中。格式为X-Forwarded-For:client1,proxy1,proxy2,一般情况下,第一个ip为客户端真实ip,后面的为经过的代理服务器的ip。现在大部分的代理都会加上这个请求头。
  • Proxy-Client-IP/WLProxy-Client-IP 这个一般是经过apache http服务器的请求才会有,用apache http做代理时一般会加上Proxy-Client-IP请求头,而WL-Proxy-Client-IP是他的weblogic插件加上的头。
  • HTTP_CLIENT_IP 有些代理服务器会加上此请求头。
  • X-Real-IP nginx代理一般会加上此请求头。

通过ip获取位置

通过上面返还的ip地址进行解析,具体方式可以参考Ip2region的官方文档。

package com.wonder.utils;

import com.alibaba.fastjson.JSONObject;
import org.lionsoul.ip2region.xdb.Searcher;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;


public class FromIpToCtiy {
    //https://github.com/lionsoul2014/ip2region/tree/master/binding/java
    public static String  resLocation(String ip) {
        String dbPath = "src/main/resources/ip2region.xdb";
        // 1、从 dbPath 中预先加载 VectorIndex 缓存,并且把这个得到的数据作为全局变量,后续反复使用。
        byte[] vIndex;
        try {
            vIndex = Searcher.loadVectorIndexFromFile(dbPath);
        } catch (Exception e) {
            System.out.printf("failed to load vector index from `%s`: %s\n", dbPath, e);
            return "{\"error\":\"加载索引失败\"}";
        }

        // 2、使用全局的 vIndex 创建带 VectorIndex 缓存的查询对象。
        Searcher searcher;
        try {
            searcher = Searcher.newWithVectorIndex(dbPath, vIndex);
        } catch (Exception e) {
            System.out.printf("failed to create vectorIndex cached searcher with `%s`: %s\n", dbPath, e);
            return "{\"error\":\"创建缓存搜索器失败\"}";
        }
        // 3、查询
        long sTime;
        String region = null;
        long cost = 0;
        try {
            sTime= System.nanoTime();
            region= searcher.search(ip);
            cost= TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
            System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
            // 4、关闭资源
            searcher.close();
        } catch (Exception e) {
            System.out.printf("failed to search(%s): %s\n", ip, e);
        }
        Map<String,String>  resmap = new HashMap<>();
        resmap.put("location",region);
        resmap.put("ioCount", String.valueOf(searcher.getIOCount()));
        resmap.put("cost", cost+"μs");
        return JSONObject.toJSONString(resmap);
        // 备注:每个线程需要单独创建一个独立的 Searcher 对象,但是都共享全局的制度 vIndex 缓存。
    }
}

说实在的,其实没啥技术含量,就是互相调用查看的,通过客户端的请求进行解析。然后查询地址的,也就是通过一个人家写的库进行查询。很简单的。欸。。目前没啥好想法了。

]]>