Compare commits

...

10 Commits

Author SHA1 Message Date
27195c0448 完善 速度显示 及 动画支持 2023-07-25 15:53:20 +08:00
fc26de7744 定位记录 demo 2023-07-25 02:12:00 +08:00
c61f6fcc7e hash 添加 crc32 crc64 支持 2023-07-24 11:01:18 +08:00
3bf4b7baa4 调整 2023-07-24 10:46:23 +08:00
9eb5d2bd96 添加 file hash duplicate 支持 2023-07-24 10:40:12 +08:00
ac62099b73 调整 2023-07-24 08:37:37 +08:00
450e4940b2 多线程分片下载 2023-07-24 00:15:00 +08:00
74cccba197 多线程分片下载 2023-07-23 22:45:01 +08:00
0f510c10db 调整 druid 驱动参数 2023-07-23 18:03:51 +08:00
7983aa26f9 定时器修正 2023-07-23 17:27:36 +08:00
40 changed files with 1822 additions and 14 deletions

View File

@ -10,12 +10,18 @@ declare module 'vue' {
CPUUsage: typeof import('./src/components/system/cpu/CPUUsage.vue')['default']
CPUUsageChart: typeof import('./src/components/system/cpu/CPUUsageChart.vue')['default']
ElButton: typeof import('element-plus/es')['ElButton']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElCol: typeof import('element-plus/es')['ElCol']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElStatistic: typeof import('element-plus/es')['ElStatistic']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTableV2: typeof import('element-plus/es')['ElTableV2']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']

View File

@ -4,6 +4,14 @@
<meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>wails</title>
<script type="text/javascript">
window._AMapSecurityConfig = {
// serviceHost:'您的代理服务器域名或地址/_AMapService',
// 例如 serviceHost:'http://1.1.1.1:80/_AMapService',
securityJsCode: "5d02234aa9de86567268e33be3c22aef"
}
</script>
</head>
<body>
<div id="app"></div>

View File

@ -19,7 +19,10 @@
"vite": "^4.0.0"
},
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@element-plus/icons-vue": "^2.1.0",
"@vueuse/core": "^10.2.1",
"axios": "^1.4.0",
"echarts": "^5.4.2",
"element-plus": "^2.3.7",
"eruda": "^3.0.0",

View File

@ -1 +1 @@
d09bd561bfdf7ff46efd368fe8aa4abe
56a069904657d2e7c850e467b8924bd9

View File

@ -1,6 +1,6 @@
<script setup>
import {useCpuUsage} from "src/utils/system/cpu";
import {computed, onMounted, reactive, ref, watch} from "vue";
import {computed, onActivated, onMounted, reactive, ref, watch} from "vue";
import * as echarts from "echarts";
import moment from "moment";
const props = defineProps({
@ -109,6 +109,10 @@ onMounted(()=>{
chartInst.setOption(chart)
chartInst.resize()
onActivated(()=>{
chartInst.resize()
})
watch(singleUsage,(v)=>{
let data = chart.series[0].data
data.push({
@ -124,7 +128,6 @@ onMounted(()=>{
chartInst.resize()
})
})
</script>
<template>

View File

@ -3,7 +3,7 @@ import {
useMemoryTotalSizeWithByte,
useMemoryUsageSizeWithByte
} from "src/utils/system/memory";
import {computed, onMounted, reactive, ref, watch} from "vue";
import {computed, onActivated, onMounted, reactive, ref, watch} from "vue";
import * as echarts from "echarts";
import moment from "moment";
import {bytesToSizeWithUnit} from "src/utils/file/file";
@ -133,6 +133,10 @@ onMounted(()=>{
chartInst.setOption(chart)
chartInst.resize()
onActivated(()=>{
chartInst.resize()
})
watch(memoryUsage,(v)=>{
chart.visualMap = getVisualMap()
let data = chart.series[0].data

View File

@ -1,5 +1,5 @@
<script setup>
import {onMounted, reactive, ref, watch} from "vue";
import {onActivated, onMounted, reactive, ref, watch} from "vue";
import * as echarts from "echarts";
import moment from "moment";
import {bytesToSizeWithUnit} from "src/utils/file/file";
@ -118,6 +118,10 @@ onMounted(()=>{
chartInst.setOption(chart)
chartInst.resize()
onActivated(()=>{
chartInst.resize()
})
watch(netWorkSumRate,(v)=>{
let upload = chart.series[0].data
upload.push({

View File

@ -31,6 +31,14 @@ const routes = [
groups: "/",
}
},
{
path: "/location-record",
component: ()=>import("src/views/tabs/location-record/index.vue"),
meta: {
title: "定位记录",
groups: "/"
}
},
{
path: "/test",
component: ()=>import("src/views/tabs/home/Test.vue"),

View File

@ -0,0 +1,62 @@
import axios, {AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig} from "axios";
import {ElNotification} from "element-plus";
export function getAxiosInstance(axiosRequestConfig: AxiosRequestConfig): AxiosInstance {
let instance = axios.create(axiosRequestConfig);
// 全局 请求拦截器
instance.interceptors.request.use(async (config: InternalAxiosRequestConfig) => {
let headers = config.headers || {}
return config;
})
// 全局 响应拦截器
instance.interceptors.response.use(successCB, rejectCB);
return instance;
}
// 请求成功回调
export function successCB(response: AxiosResponse | Promise<AxiosResponse>): AxiosResponse | Promise<AxiosResponse> {
function processResponse(response: AxiosResponse) {
if (response.status === 200) { // 响应成功
if (response.data.code === 401) {
ElNotification.warning({
message: "登录状态失效, 请重新登录",
})
console.log(response,response.data.msg)
} else if (response.data.code === 403) {
setTimeout(() => {
console.log(response,response.data.msg)
}, 0)
}
return response;
} else {
return Promise.reject(response);
}
}
if (response instanceof Promise) {
let resp: AxiosResponse
return new Promise((resolve)=>{
response.then((r: AxiosResponse) => {
resp = r;
resolve(processResponse(resp))
});
})
} else {
return processResponse(response);
}
}
// 请求失败回调
export function rejectCB(error: any): any {
let response = error.response
if(response.status === 401){
return response;
} else if(response.status === 403){
setTimeout(() => {
console.log(response.data.msg)
}, 0)
return response;
}
return Promise.reject(error);
}

View File

@ -1,4 +1,4 @@
import {computed, reactive, ref, toRefs, toValue} from "vue";
import {reactive} from "vue";
import {net} from "frontend/wailsjs/go/models";
import IOCountersStat = net.IOCountersStat;
import {GetNetworkCounter} from "frontend/wailsjs/go/system/InfoUtils";
@ -30,6 +30,7 @@ const network = reactive({
function loopNetworkSumCounter(){
clearInterval(network.sumCounter.interval)
let delay = 1000
function loop(){
let fixed = 0
GetNetworkCounter(true).then(result=> {
@ -52,15 +53,15 @@ function loopNetworkSumCounter(){
network.sumCounter.data = result[0]
fixed = Math.max(1000 - now.diff(network.sumCounter.lastTime,'ms'), 0)
// 修正计时器延迟
fixed = Math.max(delay - now.diff(network.sumCounter.lastTime,'ms'), 0)
network.sumCounter.lastTime = now
})
// 每秒调用一次
network.sumCounter.interval = setTimeout(()=>{
loop()
},1000 - fixed)
},delay - fixed)
}
loop()
}

View File

@ -151,6 +151,7 @@ const cpuInfo = useCpuInfo()
<el-button @click="addTab()">添加tab</el-button>
<el-button @click="router.push('/environment')">环境变量</el-button>
<el-button @click="router.push('/location-record')">定位记录</el-button>
</template>
<style scoped>

View File

@ -0,0 +1,32 @@
import {getAxiosInstance} from "src/utils/axios";
export const http = getAxiosInstance({
timeout: 30 * 1000
})
export default http;
export interface locationRecordListDTO {
startTime?:string,
endTime?:string
}
export const locationRecord = {
list: (dto:locationRecordListDTO, server: string)=>{
return http.post(`${server}/location/record/list`, dto,{
headers: {
token: localStorage.getItem("token")
}
})
}
}
export interface LoginDTO {
account: string,
password: string
}
export const auth = {
login: (dto:LoginDTO, server: string)=>{
return http.post(`${server}/auth/login`, dto)
}
}

View File

@ -0,0 +1,506 @@
<template>
<div class=" w-full h-full">
<div class="flex justify-start flex-wrap items-center">
<div class="flex" v-if="!view.loginStatus">
<el-input v-model="authModel.account" class="mx-1" label="账号" dense clearable clear-icon="close" filled />
<el-input v-model="authModel.password" class="mx-1" label="密码" type="password" dense clearable clear-icon="close" filled />
<el-button @click="login">登录</el-button>
</div>
<div class="flex" v-else>
<el-button @click="logout">切换账号</el-button>
</div>
<div class="flex items-center">
<el-date-picker
v-model="date"
type="datetimerange"
/>
<el-button @click="search">查询</el-button>
</div>
</div>
<div class="flex justify-end flex-wrap items-center p-1">
显示所有路径点
<el-switch
v-model="options.showMarker"
color="primary"
@update:model-value="toggleShowMarker"
keep-color
/>
仅使用高精度定位
<el-switch
v-model="options.useHighAccuracyLocation"
color="primary"
@update:model-value="toggleUseHighAccuracyLocation"
keep-color
/>
自动缩放
<el-switch
v-model="options.autoScaling"
color="primary"
@update:model-value="toggleAutoView"
keep-color
/>
卫星地图
<el-switch
v-model="layers.satellite.show"
color="primary"
@update:model-value="toggleSatellite"
keep-color
/>
路网
<el-switch
v-model="layers.roadNet.show"
color="primary"
@update:model-value="toggleRoadNet"
keep-color
/>
实时交通
<el-switch
v-model="layers.traffic.show"
color="primary"
@update:model-value="toggleTraffic"
keep-color
/>
纠偏路线
<el-switch
v-model="feature.showFixed"
color="primary"
@update:model-value="fixedLocation"
keep-color
/>
</div>
<div ref="containerRef" id="map-container" style="height: 80vh">
<div class="absolute bg-amber-50 rounded-br z-50 text-center" style="width: 300px" v-if="view.filterData.length > 0">
<div class="flex">
<div class="w-1/4">最高速度</div>
<div>{{view.speed.max}} km/h</div>
</div>
<div class="flex">
<div class="w-1/4">平均速度</div>
<div>{{view.speed.avg}} km/h</div>
</div>
</div>
<div class="absolute bottom-0 right-0 bg-amber-50 rounded-tl z-50 text-left pl-1.5" style="width: 350px" v-if="view.lines.length > 0">
<div class="flex justify-between">
<div>
路径数量: {{view.lines.length}}
</div>
<div>
<el-button dense @click="changeAnimateRate">动画速率: {{view.animateRate}}</el-button>
</div>
</div>
<el-scrollbar :style="{height: view.lines.length * 25 > 350 ?'350px':view.lines.length * 25 + 'px'}" class="overflow-auto">
<el-checkbox-group style="height: 25px" v-model="options.showLines" @update:model-value="changeShowLines" :options="options.lines" type="checkbox" dense>
<el-row v-for="opt in options.lines">
<el-col style="height: 25px">
<div>
<el-checkbox :label="opt.value">
<span style="height: 25px">{{ opt.label }}</span>
<el-icon v-if="view.animating === opt.value"
@click.prevent="stop"
color="red"
size="1rem">
<SwitchButton />
</el-icon>
<el-icon v-else @click.prevent="play(opt)" color="teal" size="1rem" class="q-ml-sm">
<VideoPlay/>
</el-icon>
</el-checkbox>
</div>
</el-col>
</el-row>
</el-checkbox-group>
</el-scrollbar>
</div>
</div>
</div>
</template>
<script setup>
import {VideoPlay,SwitchButton} from "@element-plus/icons-vue"
import AMapLoader from '@amap/amap-jsapi-loader';
import {onActivated, onMounted, onUnmounted, reactive, ref} from "vue";
import moment from "moment";
import {locationRecord} from "src/views/tabs/location-record/api";
import {getAvgSpeed, getMaxSpeed} from "src/views/tabs/location-record/logic/speed";
import {
checkLoginStatus,
loginRequest,
logoutHandler
} from "src/views/tabs/location-record/logic/login";
import {fixedLocationHandler} from "src/views/tabs/location-record/logic/location";
import {drawMarkers} from "src/views/tabs/location-record/logic/markers";
import {drawLines} from "src/views/tabs/location-record/logic/lines";
import {pauseAnimation, playAnimation, stopAnimation} from "src/views/tabs/location-record/logic/play";
import {ElMessage} from "element-plus";
function login(){
loginRequest(authModel, options.server).finally(()=>{
view.loginStatus = checkLoginStatus()
})
}
function logout(){
logoutHandler()
view.loginStatus = checkLoginStatus()
}
const date = ref([
moment().startOf('d').toDate(),
moment().toDate()
])
const feature = reactive({
showFixed: false
})
function changeAnimateRate(){
const animateRate = [1,2,5,10,20,30,60,120]
let curIndex = animateRate.findIndex(item=>item === view.animateRate)
let nextIndex = curIndex + 1
view.animateRate = nextIndex >= animateRate.length ? animateRate[0] : animateRate[nextIndex]
if(view.animating !== -1){
let index = view.animating
pauseAnimation(data)
setTimeout(()=>{
play(options.lines[index])
},100)
}
}
function stop(){
stopAnimation(map,data,view)
}
function play(item){
if(view.animating !== item.value){
stop()
}
console.log(item,view.lines,data.polylines)
options.showLines = [item.value]
changeShowLines()
view.animating = item.value
playAnimation(map,data,view.lines[item.value],view.animateRate,view,item.value)
}
// vue
let map = {}
const data = {
data: [],
polylines: [],
fixedPolyline: {},
labelsLayer: null,
markers: [],
tmpMarker: null,
infoWindow: null,
animateMarker: null,
}
const view = reactive({
filterData: [],
speed: {
max: 0,
avg: 0
},
lines: [],
loginStatus: false,
animateRate: 1,
animating: -1
})
onMounted(()=>{
view.loginStatus = checkLoginStatus()
})
onUnmounted(()=>{
clearMap()
map.remove(data.labelsLayer || {})
map.remove(data.tmpMarker || {})
map.remove(data.animateMarker || {})
map.remove(data.infoWindow || {})
})
const layers = reactive({
satellite: {
inst: {},
show: true,
},
roadNet: {
inst: {},
show: false,
},
traffic: {
inst: {},
show: false,
}
})
const options = reactive({
server: "http://10.10.10.200:45680",
useHighAccuracyLocation: true,
showMarker: false,
autoScaling: true,
lines: [],
showLines: [],
})
const authModel = reactive({
account: "",
password: "",
})
function toggleAutoView(v){
if(v){
map.setFitView()
}
}
function toggleShowMarker(v){
if(v){
options.autoScaling = false
}
redrawMap()
}
function redrawMap(){
clearMap()
drawMap()
}
function toggleUseHighAccuracyLocation(){
redrawMap()
}
function toggleSatellite(){
if (layers.satellite.show) {
layers.satellite.inst.show()
} else {
layers.satellite.inst.hide()
}
}
function toggleRoadNet() {
if (layers.roadNet.show) {
layers.roadNet.inst.show()
} else {
layers.roadNet.inst.hide()
}
}
function toggleTraffic() {
if (layers.traffic.show) {
layers.traffic.inst.show()
} else {
layers.traffic.inst.hide()
}
}
function changeShowLines(){
console.log("options.showLines",options.showLines)
console.log("view.lines",view.lines)
let showLines = view.lines.filter((_,index)=>options.showLines.includes(index))
drawMarkers(map,data,options,showLines)
drawLines(map,data,showLines)
view.speed.max = (getMaxSpeed(showLines.flat()) * 3.6).toFixed(2);
view.speed.avg = (getAvgSpeed(showLines.flat()) * 3.6).toFixed(2);
}
const containerRef = ref()
// onActivated(()=>{
onMounted(()=>{
console.log("初始化")
AMapLoader.load({
"key": "e01d7df3a4c3a1b8f06fa4544ddbbe9c", // WebKey load
"version": "2.0", // JSAPI 1.4.15
"plugins": [
"AMap.Scale",
"AMap.HawkEye",
"AMap.ToolBar",
"AMap.ControlBar",
"AMap.MoveAnimation"
],// 使'AMap.Scale'
"lang": "zh_cn"
}).then((AMap) => {
let scale = new AMap.Scale({
visible: true
})
let toolBar = new AMap.ToolBar({
visible: true,
position: {
top: '110px',
right: '40px'
}
})
let controlBar = new AMap.ControlBar({
visible: true,
position: {
top: '10px',
right: '10px'
}
})
map = new AMap.Map(containerRef.value,{
resizeEnable: true,
viewMode:'3D', //
center: [116.397428, 39.90923],
zoom: 14,
});
console.log("地图对象", map)
map.on("moveend",()=>{
let bounds = map.getBounds()
data.markers.forEach(marker=>{
if(bounds.contains(marker.getPosition())) {
marker.show()
} else {
marker.hide()
}
})
})
map.addControl(scale)
map.addControl(toolBar)
map.addControl(controlBar)
//
layers.satellite.inst = new AMap.TileLayer.Satellite({
visible: layers.satellite.show,
})
//
layers.roadNet.inst = new AMap.TileLayer.RoadNet({
visible: layers.roadNet.show,
index: 10
})
//
layers.traffic.inst = new AMap.TileLayer.Traffic({
zIndex: 11,
visible: layers.traffic.show,
});
map.addLayer(layers.satellite.inst)
map.addLayer(layers.roadNet.inst)
map.addLayer(layers.traffic.inst)
}).catch(e => {
ElMessage.error({
message: "地图组件初始化失败",
})
console.log(e);
})
// })
})
function clearMap(){
view.lines = []
map.remove(data.polylines)
map.remove(data.markers)
}
function drawMap(autoSelect=false){
if(options.useHighAccuracyLocation){
console.log("[仅使用高精度定位数据]")
data.data = data.data.filter(item=>item.locationType === 1)
}
data.data = data.data.map(item=>{
return {
time: moment(item.locationTime),
...item,
}
})
data.data.sort((a,b)=>{
return a.time.isBefore(b.time)?-1:1
})
view.filterData = data.data;
view.speed.max = (getMaxSpeed(view.filterData) * 3.6).toFixed(2);
view.speed.avg = (getAvgSpeed(view.filterData) * 3.6).toFixed(2);
console.log("data", data.data)
view.lines = []
let prev = moment(0)
for (let i = 0; i < data.data.length; i++) {
let datum = data.data[i]
if (prev.year() === datum.time.year() && prev.month() === datum.time.month() && prev.date() === datum.time.date() && datum.time.diff(prev,'m') <= 5 ) {
prev = datum.time
} else {
prev = datum.time
view.lines.push([])
}
view.lines[view.lines.length -1].push(datum)
}
view.lines = view.lines.reverse()
options.lines = view.lines.map((item,index)=>{
if(autoSelect){
//
options.showLines.push(index)
}
return {
label: `${item[0].locationTime} ~ ${item[item.length-1].locationTime}`,
value: index,
}
})
console.log("lines", view.lines)
drawMarkers(map,data,options,view.lines.filter((_,index)=>options.showLines.includes(index)))
drawLines(map,data,view.lines.filter((_,index)=>options.showLines.includes(index)))
setTimeout(()=>{
if(!options.autoScaling){
return
}
if (data.polylines.length > 0) {
map.setFitView()
}
})
fixedLocation()
}
function search() {
console.log(date.value)
view.animating = -1
let params = {
startTime: !date.value[0] ? undefined : moment(date.value[0]).format("YYYY-MM-DD HH:mm:ss"),
endTime: !date.value[1] ? undefined : moment(date.value[1]).format("YYYY-MM-DD HH:mm:ss"),
}
clearMap();
locationRecord.list(params, options.server).then(resp => {
if(resp.status !== 200){
console.log(resp)
let res = resp.data || {}
ElMessage.error({
message: res.data || res.msg || "请求失败",
})
throw new Error("请求失败")
}
return resp.data;
}).then(res => {
data.data = res.data || []
options.showLines = []
drawMap(true)
})
}
function fixedLocation() {
map.remove(data.fixedPolyline)
if (!feature.showFixed) {
return
}
fixedLocationHandler(map,data)
}
</script>
<style scoped lang="scss">
#container {
width: 100%;
height: 80vh;
}
</style>

View File

@ -0,0 +1,10 @@
export const COLOR = [
'#6090e0', '#30c030', '#d070d0', '#80c0f0', '#f07050', '#ffb900',
'#37a2da', '#ffdb5c', '#8378ea', '#e7bcf3', '#32c5e9', '#67e0e3',
'#dd6b66', '#759aa0', '#e69d87', '#8dc1a9', '#ea7e53', '#eedd78',
]
export function randomColorList(len:number){
return Array.from({ length: len })
.map(() => COLOR[Math.floor(Math.random() * COLOR.length)]);
}

View File

@ -0,0 +1,18 @@
export function createCanvasDir(){
let canvasDir = document.createElement('canvas')
let width = 24;
let height = 24;
canvasDir.width = width;
canvasDir.height = height;
let context = canvasDir.getContext('2d')!;
context.strokeStyle = 'white';
context.lineJoin = 'round';
context.lineWidth = 8;
context.moveTo(-4, width - 4);
context.lineTo(width / 2, 6);
context.lineTo(width + 4, width - 4);
context.stroke();
return canvasDir;
}

View File

@ -0,0 +1,72 @@
import {randomColorList} from "src/views/tabs/location-record/logic/color";
import {createCanvasDir} from "src/views/tabs/location-record/logic/icon";
import {getAvgSpeed, getMaxSpeed} from "src/views/tabs/location-record/logic/speed";
const canvasDir = createCanvasDir();
const SLOW = "#ff0036"
const NORMAL = "#ff8119"
const FAST = "#AF5"
export function drawLines(map,data,lines){
map.remove(data.polylines)
data.polylines = []
let colorList = randomColorList(lines.length)
lines.forEach((_data,index) => {
let max = getMaxSpeed(_data)
let avg = getAvgSpeed(_data)
let p:any = []
for (let i = 0; i < _data.length; i++) {
let item = _data[i]
let speed = item.speed || 0
let type
if(speed < avg / 2){
type= SLOW
} else if(speed >= avg/2 && speed < avg){
type = NORMAL
} else {
type = FAST
}
if(p.length === 0){
p.push({
path: [new AMap.LngLat(item.longitude, item.latitude)],
strokeColor: type,
})
} else {
if(p[p.length - 1].strokeColor === type){
p[p.length - 1].path.push(new AMap.LngLat(item.longitude, item.latitude))
} else {
let prevPath = p[p.length - 1].path
let prev = prevPath[prevPath.length - 1]
p.push({
path: [prev, new AMap.LngLat(item.longitude, item.latitude)],
strokeColor: type,
})
}
}
}
console.log("p",p)
let polylineArr:any[] = []
p.forEach((item)=>{
let polyline = new AMap.Polyline({
path: item.path,
showDir: true,
dirImg: canvasDir,
borderWeight: 6, // 线条宽度,默认为 1
strokeWeight: 6,
strokeOpacity: 0.9, // 透明度
strokeColor: item.strokeColor, // 线条颜色
dirColor: 'white',
lineJoin: 'round' // 折线拐点连接处样式})
})
polylineArr.push(polyline)
})
data.polylines.push(polylineArr)
})
data.polylines = data.polylines.flat()
console.log("polylines",data.polylines)
map.add(data.polylines)
}

View File

@ -0,0 +1,65 @@
export function fixedLocationHandler(map,data) {
console.log(map,data)
let task:Promise<any>[] = []
let startTime
let preFixedData = (data.data || []).map((item, index) => {
if (index % 500 === 0) {
startTime = data.data[0].time
}
let time = item.time
return {
x: item.longitude,
y: item.latitude,
sp: item.speed || 0,
ag: item.bearing || 0,
tm: index % 500 === 0 ? time.unix() : time.diff(startTime, 's')
}
})
console.log("纠偏路线 预处理数据: ", preFixedData)
let group:any[] = []
let len = preFixedData.length
for (let i = 0; i < Math.ceil(len / 500); i++) {
group.push(preFixedData.splice(0, 500))
}
console.log("分组: ", group)
task = group.map(item => {
return new Promise(resolve => {
AMap.plugin('AMap.GraspRoad', function () {
let grasp = new AMap.GraspRoad();
grasp.driving(item, function (error, result) {
if (error) {
resolve([])
}
if (!error) {
let newPath = result.data.points;//纠偏后的轨迹
let distance = result.data.distance;//里程
console.log("返回值 => ", "路径: ", newPath, "里程: ", distance)
resolve(newPath)
}
})
})
})
})
Promise.all(task).then(paths => {
let fixedData = paths.flat(1)
console.log("纠偏路线数据: ", fixedData)
data.fixedPolyline = new AMap.Polyline({
path: fixedData.map(item => {
return new AMap.LngLat(item.x, item.y)
}),
showDir: true,
strokeColor: 'orange', // 线条颜色
borderWeight: 6, // 线条宽度,默认为 1
strokeWeight: 6,
})
map.add(data.fixedPolyline)
})
}

View File

@ -0,0 +1,54 @@
import {auth, LoginDTO} from "src/views/tabs/location-record/api";
import moment from "moment";
import {ElMessage, ElNotification} from "element-plus";
export function checkLoginStatus(){
let user = localStorage.getItem("user")
let lastLogin = localStorage.getItem("lastLogin")
let token = localStorage.getItem("token")
return !!user &&
!!lastLogin &&
!!token &&
moment().diff(moment(lastLogin),"h") < 24
}
export function logoutHandler(){
localStorage.removeItem("user")
localStorage.removeItem("lastLogin")
localStorage.removeItem("token")
}
export function loginRequest(authModel:LoginDTO, server:string){
logoutHandler()
return new Promise((resolve)=>{
auth.login(authModel,server).then((resp)=>{
let res = resp.data
console.log("login", res)
if(res.code !== 200){
ElMessage.warning({
message: res.data || res.msg,
duration: 500
})
resolve(false)
} else {
ElMessage.success({
message: "登录成功",
duration: 500
})
localStorage.setItem("user", authModel.account)
localStorage.setItem("lastLogin", moment().format("YYYY-MM-DD HH:mm:ss"))
localStorage.setItem("token",res.data.token)
resolve(true)
}
}).catch((e)=>{
console.error(e)
ElMessage.error({
message: "登录失败",
duration: 500,
})
resolve(false)
})
})
}

View File

@ -0,0 +1,91 @@
export function getMarkers(map,data,line){
if(!data.tmpMarker){
data.tmpMarker = new AMap.Marker({
anchor: 'bottom-center',
offset: [0, -15],
});
}
return line.filter((_, index) => index % 1 === 0).map((item) => {
let text = {
fontSize: 10,
content: `时间:${item.locationTime}<br>速度: ${((item.speed || 0) * 3.6).toFixed(2)}km/h 方向角: ${(item.bearing || 0).toFixed(2)}°`
}
let marker = new AMap.LabelMarker({
name: item.id,
position: [item.longitude, item.latitude],
zIndex: 10,
icon: {
// 图标类型,现阶段只支持 image 类型
type: 'image',
// 图片 url
image: 'https://webapi.amap.com/theme/v1.3/markers/n/mark_b.png',
// 图片尺寸
size: [6, 9],
// 图片相对 position 的锚点,默认为 bottom-center
anchor: 'bottom-center'
},
// text,
})
function show(e) {
let position = e.data.data && e.data.data.position;
if (position) {
data.tmpMarker.setContent(
`<div class="amap-info-window"
style="padding: .75rem 1.25rem;font-size:12px;
margin-bottom: 1rem;
border-radius: .25rem;
position: fixed;
top: 1rem;
background-color: white;
width: auto;
min-width: 22rem;
border-width: 0;
right: 1rem;
box-shadow: 0 2px 6px 0 rgba(114, 124, 245, .5);">`
+ text.content +
'<div class="amap-info-sharp"></div>' +
'</div>');
data.tmpMarker.setPosition(position);
map.add(data.tmpMarker);
}
}
function hide() {
map.remove(data.tmpMarker);
}
marker.on('mouseover', (e) => show(e));
marker.on('touchstart', (e) => show(e));
marker.on('mouseout', () => hide());
marker.on('touchend', () => hide());
return marker;
});
}
export function drawMarkers(map,data,options,lines){
if(!data.labelsLayer){
data.labelsLayer = new AMap.LabelsLayer({
zooms: [3, 20],
zIndex: 1000,
// 该层内标注是否避让
collision: true,
// 设置 allowCollisiontrue可以让标注避让用户的标注
allowCollision: false,
});
map.add(data.labelsLayer)
}
data.labelsLayer.remove(data.markers)
data.markers = []
if(!options.showMarker){
return
}
data.markers = lines.map(line=>{
return getMarkers(map,data,line)
}).flat()
data.labelsLayer.add(data.markers)
}

View File

@ -0,0 +1,84 @@
let index = 0
export function playAnimation(map,data,line,animateRate,view,itemIndex){
console.log("playAnimation",line)
if(data.animateMarker){
map.remove(data.animateMarker)
}
data.animateMarker = new AMap.Marker({
map: map,
position: [0,0],
icon: "https://a.amap.com/jsapi_demos/static/demo-center-v2/car.png",
offset: new AMap.Pixel(-13, -26),
autoRotation: true
})
if(!data.infoWindow){
data.infoWindow = new AMap.InfoWindow({
offset: new AMap.Pixel(6, -25),
content: "",
isCustom: true
});
}
if(itemIndex !== view.animating){
index = 0
}
let path = line.slice(index).map((item) => {
return new AMap.LngLat(item.longitude, item.latitude)
})
data.animateMarker.stopMove();
data.animateMarker.on("movealong",()=>{})
let lastMovingIndex = 0
data.animateMarker.on("moving",(e)=>{
if(lastMovingIndex != e.index){
index++
}
lastMovingIndex = e.index
let lastLocation = e.passedPath[e.passedPath.length - 1];
data.infoWindow.setPosition(lastLocation);
let speed = ((line[index].speed||0) * 3.6).toFixed(2)
data.infoWindow.setContent(`<div style="width:180px;background: #fff;border-radius: 5px">
<div>${line[index].locationTime}</div>
<div>${speed} km/h</div>
</div>`);
map.setCenter(e.target.getPosition(),true)
})
data.animateMarker.on("moveend",()=>{
if(index >= line.length-2){
setTimeout(()=>{
view.animating = -1
index = 0
data.animateMarker.stopMove()
map.remove(data.animateMarker)
data.infoWindow.close()
},100)
}
})
let polyline = new AMap.Polyline({
path: path,
})
data.animateMarker.moveAlong(polyline.getPath(),{
duration: 1000/animateRate,
autoRotation: true
});
data.infoWindow.open(map, data.animateMarker.getPosition())
}
export function stopAnimation(map,data,view){
if(!data.animateMarker){
return
}
pauseAnimation(data)
view.animating = -1
index = 0
}
export function pauseAnimation(data){
if(!data.animateMarker){
return
}
data.animateMarker.stopMove()
}

View File

@ -0,0 +1,14 @@
export function getMaxSpeed(data:any[]){
let sortSpeedData = JSON.parse(JSON.stringify(data)).sort((a,b)=>{return (b.speed || 0) - (a.speed||0)})
return ((sortSpeedData[0]||{}).speed || 0)
}
export function getAvgSpeed(data:any[]){
if(data.length === 0){
return 0
} else {
return ((data.map(item=>item.speed || 0).reduce((prev,cur)=>{
return prev + cur
},0) / data.length))
}
}

View File

@ -10,6 +10,7 @@
"resolveJsonModule": true,
"isolatedModules": false,
"esModuleInterop": true,
"noImplicitAny": false,
"lib": [
"ESNext",
"DOM"
@ -24,7 +25,8 @@
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
"src/**/*.vue",
"node_modules/@amap/amap-jsapi-loader/src/global.d.ts"
],
"references": [
{

View File

@ -0,0 +1,8 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function CalcMD5(arg1:string):Promise<string>;
export function CalcSHA1(arg1:string):Promise<string>;
export function RecursiveScan(arg1:string,arg2:boolean,arg3:boolean):Promise<Array<string>>;

View File

@ -0,0 +1,15 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function CalcMD5(arg1) {
return window['go']['duplicate']['Support']['CalcMD5'](arg1);
}
export function CalcSHA1(arg1) {
return window['go']['duplicate']['Support']['CalcSHA1'](arg1);
}
export function RecursiveScan(arg1, arg2, arg3) {
return window['go']['duplicate']['Support']['RecursiveScan'](arg1, arg2, arg3);
}

View File

@ -0,0 +1,11 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import {io} from '../models';
export function CopyN(arg1:io.Writer,arg2:string,arg3:number):Promise<number>;
export function IsDir(arg1:string):Promise<boolean>;
export function IsFile(arg1:string):Promise<boolean>;
export function SaveJSONFile(arg1:string,arg2:any):Promise<void>;

View File

@ -0,0 +1,19 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function CopyN(arg1, arg2, arg3) {
return window['go']['file']['Support']['CopyN'](arg1, arg2, arg3);
}
export function IsDir(arg1) {
return window['go']['file']['Support']['IsDir'](arg1);
}
export function IsFile(arg1) {
return window['go']['file']['Support']['IsFile'](arg1);
}
export function SaveJSONFile(arg1, arg2) {
return window['go']['file']['Support']['SaveJSONFile'](arg1, arg2);
}

View File

@ -0,0 +1,14 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function CalcCRC32(arg1:string,arg2:number):Promise<string>;
export function CalcCRC64(arg1:string,arg2:number):Promise<string>;
export function CalcMD5(arg1:string,arg2:number):Promise<string>;
export function CalcSHA1(arg1:string,arg2:number):Promise<string>;
export function CalcSHA256(arg1:string,arg2:number):Promise<string>;
export function CalcSHA512(arg1:string,arg2:number):Promise<string>;

View File

@ -0,0 +1,27 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function CalcCRC32(arg1, arg2) {
return window['go']['hash']['Support']['CalcCRC32'](arg1, arg2);
}
export function CalcCRC64(arg1, arg2) {
return window['go']['hash']['Support']['CalcCRC64'](arg1, arg2);
}
export function CalcMD5(arg1, arg2) {
return window['go']['hash']['Support']['CalcMD5'](arg1, arg2);
}
export function CalcSHA1(arg1, arg2) {
return window['go']['hash']['Support']['CalcSHA1'](arg1, arg2);
}
export function CalcSHA256(arg1, arg2) {
return window['go']['hash']['Support']['CalcSHA256'](arg1, arg2);
}
export function CalcSHA512(arg1, arg2) {
return window['go']['hash']['Support']['CalcSHA512'](arg1, arg2);
}

View File

@ -7,6 +7,11 @@
resolved "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30"
integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==
"@amap/amap-jsapi-loader@^1.0.1":
version "1.0.1"
resolved "https://registry.npmmirror.com/@amap/amap-jsapi-loader/-/amap-jsapi-loader-1.0.1.tgz#9ec4b4d5d2467eac451f6c852e35db69e9f9f0c0"
integrity sha512-nPyLKt7Ow/ThHLkSvn2etQlUzqxmTVgK7bIgwdBRTg2HK5668oN7xVxkaiRe3YZEzGzfV2XgH5Jmu2T73ljejw==
"@ampproject/remapping@^2.2.0":
version "2.2.1"
resolved "https://registry.npmmirror.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630"
@ -289,7 +294,7 @@
resolved "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.0.tgz#53fa5fe9c34faee89469e48f91d51a3766108bc8"
integrity sha512-/Z3l6pXthq0JvMYdUFyX9j0MaCltlIn6mfh9jLyQwg5aPKxkyNa0PTHtU1AlFXLNk55ZuAeJRcpvq+tmLfKmaQ==
"@element-plus/icons-vue@^2.0.6":
"@element-plus/icons-vue@^2.0.6", "@element-plus/icons-vue@^2.1.0":
version "2.1.0"
resolved "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.1.0.tgz#7ad90d08a8c0d5fd3af31c4f73264ca89614397a"
integrity sha512-PSBn3elNoanENc1vnCfh+3WA9fimRC7n+fWkf3rE5jvv+aBohNHABC/KAR5KWPecxWxDTVT1ERpRbOMRcOV/vA==
@ -728,6 +733,11 @@ async-validator@^4.2.5:
resolved "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz#c96ea3332a521699d0afaaceed510a54656c6339"
integrity sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
autoprefixer@^10.4.14:
version "10.4.14"
resolved "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.14.tgz#e28d49902f8e759dd25b153264e862df2705f79d"
@ -740,6 +750,15 @@ autoprefixer@^10.4.14:
picocolors "^1.0.0"
postcss-value-parser "^4.2.0"
axios@^1.4.0:
version "1.4.0"
resolved "https://registry.npmmirror.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f"
integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==
dependencies:
follow-redirects "^1.15.0"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
@ -833,6 +852,13 @@ color-name@1.1.3:
resolved "https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
commander@^4.0.0:
version "4.1.1"
resolved "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
@ -870,6 +896,11 @@ debug@^4.1.0, debug@^4.3.4:
dependencies:
ms "2.1.2"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
didyoumean@^1.2.2:
version "1.2.2"
resolved "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
@ -992,6 +1023,20 @@ fill-range@^7.0.1:
dependencies:
to-regex-range "^5.0.1"
follow-redirects@^1.15.0:
version "1.15.2"
resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.npmmirror.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
fraction.js@^4.2.0:
version "4.2.0"
resolved "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
@ -1196,6 +1241,18 @@ micromatch@^4.0.4, micromatch@^4.0.5:
braces "^3.0.2"
picomatch "^2.3.1"
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.12:
version "2.1.35"
resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
minimatch@^3.0.4:
version "3.1.2"
resolved "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
@ -1362,6 +1419,11 @@ postcss@^8.1.10, postcss@^8.4.23, postcss@^8.4.24:
picocolors "^1.0.0"
source-map-js "^1.0.2"
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"

View File

@ -9,6 +9,7 @@ require (
github.com/go-playground/locales v0.14.1
github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.14.1
github.com/goccy/go-json v0.10.2
github.com/gookit/goutil v0.6.11
github.com/mutecomm/go-sqlcipher/v4 v4.4.2
github.com/pelletier/go-toml/v2 v2.0.8
@ -38,7 +39,6 @@ require (
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.3.0 // indirect

View File

@ -9,6 +9,9 @@ import (
"skapp/pkg/sdk/config"
"skapp/pkg/sdk/dialog"
"skapp/pkg/sdk/env"
fileSdk "skapp/pkg/sdk/file"
"skapp/pkg/sdk/file/duplicate"
"skapp/pkg/sdk/file/hash"
"skapp/pkg/sdk/system"
"skapp/pkg/sdk/utils"
)
@ -19,7 +22,9 @@ var assets embed.FS
func main() {
// Create an instance of the app structure
app := core.NewApp()
fileSupport := fileSdk.New()
hashSupport := hash.New(fileSupport)
duplicateSupport := duplicate.New(fileSupport, hashSupport)
// Create application with options
err := wails.Run(&options.App{
Title: "wails",
@ -40,6 +45,9 @@ func main() {
&system.InfoUtils{},
dialog.New(app),
&config.Support{},
fileSupport,
hashSupport,
duplicateSupport,
},
Debug: options.Debug{
OpenInspectorOnStartup: true,

View File

@ -0,0 +1,28 @@
package downloader
import (
"fmt"
"log"
"os"
"testing"
"time"
)
func TestDownloader(t *testing.T) {
url := "https://mirrors.tuna.tsinghua.edu.cn/Adoptium/8/jdk/x64/windows/OpenJDK8U-jdk_x64_windows_hotspot_8u382b05.zip"
dir := os.TempDir()
fmt.Println(dir)
fileName := "OpenJDK8U-jdk_x64_windows_hotspot_8u382b05.zip"
a, _ := getRedirectInfo(url, UserAgent)
location := a.Header.Get("Location")
if len(location) == 0 {
location = url
}
startTime := time.Now()
d := NewDownloader(location, dir, fileName, 5)
if err := d.Run(); err != nil {
log.Fatal(err)
}
fmt.Printf("\n 文件下载完成耗时: %f second\n", time.Now().Sub(startTime).Seconds())
}

View File

@ -0,0 +1,285 @@
package downloader
import (
"bytes"
"errors"
"fmt"
"io"
"log"
"mime"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"sync"
)
const UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36"
// PartFile 文件切片
type PartFile struct {
Index int64
From int64
To int64
Data []byte
Done bool
}
type Downloader struct {
FileSize int64
Url string
FileName string
Path string
PartNum int64
donePart []PartFile
}
// NewDownloader 创建下载器
func NewDownloader(url, outputDir, outputFileName string, partNum int64) *Downloader {
if outputDir == "" {
wd, err := os.Getwd() //获取当前工作目录
if err != nil {
log.Println(err)
}
outputDir = wd
}
return &Downloader{
FileSize: 0,
Url: url,
FileName: outputFileName,
Path: outputDir,
PartNum: partNum,
donePart: make([]PartFile, partNum),
}
}
func (d *Downloader) getNewRequest(method string) (*http.Request, error) {
req, err := http.NewRequest(
method,
d.Url,
nil)
return req, err
}
func (d *Downloader) head() (int64, error) {
req, err := d.getNewRequest(http.MethodHead)
req = setNewHeader(req)
if err != nil {
return 0, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return 0, err
}
if resp.StatusCode > 299 {
return 0, errors.New(fmt.Sprintf("Can't process, response is %v", resp.StatusCode))
}
if resp.Header.Get("Accept-Ranges") != "bytes" {
return 0, errors.New("服务器不支持文件断点续传")
}
d.FileName = GetFileInfoFromResponse(resp)
length, err := strconv.Atoi(resp.Header.Get("Content-Length"))
return int64(length), err
}
// 下载切片
func (d *Downloader) downloadPart(c PartFile, f *os.File) error {
r, err := d.getNewRequest("GET")
r = setNewHeader(r)
if err != nil {
return err
}
log.Printf("开始[%d]下载from:%d to:%d\n", c.Index, c.From, c.To)
r.Header.Set("Range", fmt.Sprintf("bytes=%v-%v", c.From, c.To))
resp, err := http.DefaultClient.Do(r)
if err != nil {
return err
}
if resp.StatusCode > 299 {
return errors.New(fmt.Sprintf("服务器错误状态码: %v", resp.StatusCode))
}
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
bs, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if len(bs) != int(c.To-c.From+1) {
}
c.Data = bs
c.Done = true
d.donePart[c.Index] = c
_, err = f.WriteAt(bs, c.From)
if err != nil {
c.Done = true
}
log.Printf("结束[%d]下载", c.Index)
return err
}
func (d *Downloader) checkIntegrity(t *os.File) error {
log.Println("开始合并文件")
totalSize := 0
for _, s := range d.donePart {
//hash.Write(s.Data)
totalSize += len(s.Data)
}
if int64(totalSize) != d.FileSize {
return errors.New("文件不完整")
}
_ = t.Close()
return os.Rename(filepath.Join(d.Path, d.FileName+".tmp"), filepath.Join(d.Path, d.FileName))
}
// Run 开始下载任务
func (d *Downloader) Run() error {
fileTotalSize, err := d.head()
if err != nil {
return err
}
d.FileSize = fileTotalSize
jobs := make([]PartFile, d.PartNum)
eachSize := fileTotalSize / d.PartNum
path := filepath.Join(d.Path, d.FileName+".tmp")
tmpFile := new(os.File)
fByte := make([]byte, d.FileSize)
if exists(path) {
tmpFile, err = os.OpenFile(path, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0)
if err != nil {
return err
}
fByte, err = io.ReadAll(tmpFile)
} else {
tmpFile, err = os.Create(path)
}
if err != nil {
return err
}
defer func(tmpFile *os.File) {
_ = tmpFile.Close()
}(tmpFile)
for i := range jobs {
i64 := int64(i)
jobs[i64].Index = i64
if i == 0 {
jobs[i64].From = 0
} else {
jobs[i64].From = jobs[i64-1].To + 1
}
if i64 < d.PartNum-1 {
jobs[i64].To = jobs[i64].From + eachSize
} else {
//the last filePart
jobs[i64].To = fileTotalSize - 1
}
}
for i, j := range jobs {
tmpJob := j
emptyTmp := make([]byte, tmpJob.To-j.From)
if bytes.Compare(emptyTmp, fByte[tmpJob.From:j.To]) != 0 {
tmpJob.Data = fByte[j.From : j.To+1]
tmpJob.Done = true
d.donePart[tmpJob.Index] = tmpJob
} else {
tmpJob.Done = false
}
jobs[i] = tmpJob
}
var wg sync.WaitGroup
for _, j := range jobs {
if !j.Done {
wg.Add(1)
go func(job PartFile) {
defer wg.Done()
err := d.downloadPart(job, tmpFile)
if err != nil {
log.Println("下载文件失败:", err, job)
}
}(j)
}
}
wg.Wait()
return d.checkIntegrity(tmpFile)
}
func getRedirectInfo(u, userAgent string) (*http.Response, error) {
log.Println("获取重定向信息")
var a *url.URL
a, _ = url.Parse(u)
header := http.Header{}
//header.Add("Cookie", rawCookies)
header.Add("User-Agent", userAgent)
request := http.Request{
Header: header,
Method: "GET",
URL: a,
}
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
response, err := client.Do(&request)
if err != nil {
return nil, err
}
return response, nil
}
func setNewHeader(r *http.Request) *http.Request {
r.Header.Add("User-Agent", UserAgent)
r.Header.Add("Upgrade-Insecure-Requests", "1")
return r
}
func exists(path string) bool {
_, err := os.Stat(path) //os.Stat获取文件信息
if err != nil {
if os.IsExist(err) {
return true
}
return false
}
return true
}
func GetFileInfoFromResponse(resp *http.Response) string {
contentDisposition := resp.Header.Get("Content-Disposition")
if contentDisposition != "" {
_, params, err := mime.ParseMediaType(contentDisposition)
if err != nil {
panic(err)
}
return params["filename"]
}
filename := filepath.Base(resp.Request.URL.Path)
return filename
}

View File

@ -0,0 +1,13 @@
package downloader
import "skapp/pkg/core"
type Support struct {
app *core.App
}
func New(app *core.App) *Support {
return &Support{
app,
}
}

View File

@ -0,0 +1,86 @@
package duplicate
import (
"bufio"
"io/fs"
"os"
"path/filepath"
"skapp/pkg/sdk/file/hash"
"skapp/pkg/logger"
fileSdk "skapp/pkg/sdk/file"
)
var log = logger.Log
type Support struct {
fileSupport *fileSdk.Support
hash *hash.Support
}
func New(fileSupport *fileSdk.Support, hash *hash.Support) *Support {
return &Support{fileSupport, hash}
}
var (
maxReadSize int64 = 1024 * 1024 * 5
chunkSize = 1024 * 1024
)
// RecursiveScan 递归 目录下需要扫描的文件
func (s *Support) RecursiveScan(dir string, addFile bool, addDir bool) []string {
//nodeModules, _ := regexp.Compile("node_modules")
//if nodeModules.Match([]byte(dir)) {
// return []string{}
//}
files := make([]string, 0)
fileMap := make(map[string]bool)
//suffixReg, _ := regexp.Compile(".*[\\.j(t)s|\\.vue|\\.jsx|\\.tsx]$")
absPath, _ := filepath.Abs(dir)
_ = filepath.Walk(absPath, func(file string, info fs.FileInfo, err error) error {
if info.Mode() == os.ModeSymlink {
file, err = os.Readlink(file)
s.RecursiveScan(file, addFile, addDir)
return err
}
if addFile {
if s.fileSupport.IsFile(file) {
//if suffixReg.Match([]byte(file)) {
log.Infof("[扫描文件] 添加扫描文件 %s", file)
fileMap[file] = true
//}
}
}
if addDir {
if s.fileSupport.IsDir(file) {
log.Infof("[扫描文件] 添加扫描文件夹 %s", file)
fileMap[file] = true
}
}
return nil
})
files = make([]string, 0, len(fileMap))
for file := range fileMap {
files = append(files, file)
}
return files
}
func Reader(file *os.File) *bufio.Reader {
return bufio.NewReaderSize(file, chunkSize)
}
func (s *Support) CalcSHA1(path string) (sha1hex string, err error) {
return s.hash.CalcSHA1(path, maxReadSize)
}
func (s *Support) CalcMD5(path string) (md5hex string, err error) {
return s.hash.CalcMD5(path, maxReadSize)
}

View File

@ -0,0 +1,79 @@
package file
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"skapp/pkg/utils/json"
)
type Support struct {
}
func New() *Support {
return &Support{}
}
func (s *Support) IsFile(path string) bool {
info, err := os.Stat(path)
if err != nil {
return false
}
return !info.IsDir()
}
func (s *Support) IsDir(path string) bool {
info, err := os.Stat(path)
if err != nil {
return false
}
return info.IsDir()
}
func (s *Support) CopyN(w io.Writer, path string, n int64) (int64, error) {
if s.IsDir(path) {
return 0, errors.New(fmt.Sprintf("%s 为文件夹", path))
}
file, err := os.Open(path)
defer func(file *os.File) {
_ = file.Close()
}(file)
if err != nil {
return 0, err
}
info, _ := file.Stat()
if n <= 0 {
return io.Copy(w, file)
}
if info.Size() < n {
return io.Copy(w, file)
} else {
return io.CopyN(w, file, n)
}
}
func (s *Support) SaveJSONFile(filePath string, data interface{}) error {
if !filepath.IsAbs(filePath) {
root, _ := os.Getwd()
filePath = filepath.Join(root, filePath)
}
jsonFile, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
defer func(scmF *os.File) {
_ = scmF.Close()
}(jsonFile)
if err != nil {
return err
}
_, err = jsonFile.WriteString(json.Json(data))
return err
}

View File

@ -0,0 +1,80 @@
package hash
import (
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"hash/crc32"
"hash/crc64"
fileSdk "skapp/pkg/sdk/file"
)
type Support struct {
fileSupport *fileSdk.Support
}
func New(fileSupport *fileSdk.Support) *Support {
return &Support{fileSupport}
}
func (s *Support) CalcCRC32(path string, readSize int64) (hash string, err error) {
h := crc32.NewIEEE()
_, err = s.fileSupport.CopyN(h, path, readSize)
if err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
func (s *Support) CalcCRC64(path string, readSize int64) (hash string, err error) {
h := crc64.New(crc64.MakeTable(crc64.ECMA))
_, err = s.fileSupport.CopyN(h, path, readSize)
if err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
func (s *Support) CalcSHA512(path string, readSize int64) (hash string, err error) {
h := sha512.New()
_, err = s.fileSupport.CopyN(h, path, readSize)
if err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
func (s *Support) CalcSHA256(path string, readSize int64) (hash string, err error) {
h := sha256.New()
_, err = s.fileSupport.CopyN(h, path, readSize)
if err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
func (s *Support) CalcSHA1(path string, readSize int64) (hash string, err error) {
h := sha1.New()
_, err = s.fileSupport.CopyN(h, path, readSize)
if err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
func (s *Support) CalcMD5(path string, readSize int64) (hash string, err error) {
h := md5.New()
_, err = s.fileSupport.CopyN(h, path, readSize)
if err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}

View File

@ -0,0 +1,10 @@
package json
import (
"github.com/goccy/go-json"
)
func Json(data interface{}) string {
jsonBytes, _ := json.MarshalIndent(data, "", " ")
return string(jsonBytes)
}

View File

@ -19,6 +19,13 @@ spring:
password: 12341234
url: jdbc:mysql://10.10.10.200:3306/matrix_v2?createDatabaseIfNotExist=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
# jdbc-url: jdbc:mysql://10.10.10.100:3306/matrix?createDatabaseIfNotExist=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
initial-size: 5
min-idle: 5
maxWait: 60000
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
validationQuery: SELECT 1
rules:
sharding:

View File

@ -37,12 +37,20 @@ spring:
datasource:
names: ds
ds:
# type: com.zaxxer.hikari.HikariDataSource
# type: com.zaxxer.hikari.HikariDataSource
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 12341234
url: jdbc:mysql://10.10.10.200:3306/matrix_v2?createDatabaseIfNotExist=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true
initial-size: 5
min-idle: 5
maxWait: 60000
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
validationQuery: SELECT 1
# jdbc-url: jdbc:mysql://10.10.10.100:3306/matrix?createDatabaseIfNotExist=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
rules: