最新产品

This commit is contained in:
ZZX9599
2025-09-08 17:01:50 +08:00
commit 8056245ade
119 changed files with 8281 additions and 0 deletions

50
.gitignore vendored Normal file
View File

@ -0,0 +1,50 @@
# Maven 相关忽略
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
# IntelliJ IDEA 相关忽略
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
# Eclipse 相关忽略
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
# NetBeans 相关忽略
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
# VS Code 相关忽略
### VS Code ###
.vscode/
# Mac OS 系统文件忽略
### Mac OS ###
.DS_Store
# 新增:忽略自定义的 sdk 文件夹和 upload 文件夹
# / 表示忽略整个文件夹及其内部所有内容)
sdk/
upload/
logs/

0
.idea/.gitignore generated vendored Normal file
View File

8
.idea/encodings.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/logs/sdk.log" charset="GBK" />
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

15
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
<option name="workspaceImportForciblyTurnedOn" value="true" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

124
.idea/uiDesigner.xml generated Normal file
View File

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Palette2">
<group name="Swing">
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
</item>
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
</item>
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
</item>
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
<initial-values>
<property name="text" value="Button" />
</initial-values>
</item>
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="RadioButton" />
</initial-values>
</item>
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="CheckBox" />
</initial-values>
</item>
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
<initial-values>
<property name="text" value="Label" />
</initial-values>
</item>
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
</item>
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
</item>
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
<preferred-size width="-1" height="20" />
</default-constraints>
</item>
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
</item>
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
</item>
</group>
</component>
</project>

191
.idea/workspace.xml generated Normal file
View File

@ -0,0 +1,191 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="1ba8c79d-5980-42c3-ba31-837b4b96bb22" name="Changes" comment="" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="HTML File" />
<option value="Class" />
</list>
</option>
</component>
<component name="HighlightingSettingsPerFile">
<setting file="file://$PROJECT_DIR$/geo.html" root0="SKIP_INSPECTION" />
<setting file="file://$PROJECT_DIR$/index.html" root0="SKIP_INSPECTION" />
<setting file="file://$PROJECT_DIR$/pom.xml" root0="SKIP_INSPECTION" />
<setting file="file://$PROJECT_DIR$/src/main/java/com/yj/earth/ServerApp.java" root0="SKIP_INSPECTION" />
<setting file="file://$PROJECT_DIR$/src/main/java/com/yj/earth/aspect/RoleAccessAspect.java" root0="SKIP_INSPECTION" />
<setting file="file://$PROJECT_DIR$/src/main/java/com/yj/earth/auth/AuthValidator.java" root0="SKIP_INSPECTION" />
<setting file="file://$PROJECT_DIR$/src/main/java/com/yj/earth/business/controller/AuthController.java" root0="SKIP_INSPECTION" />
<setting file="file://$PROJECT_DIR$/src/main/java/com/yj/earth/business/controller/FileInfoController.java" root0="SKIP_INSPECTION" />
<setting file="file://$PROJECT_DIR$/src/main/java/com/yj/earth/business/controller/GraphHopperController.java" root0="SKIP_INSPECTION" />
<setting file="file://$PROJECT_DIR$/src/main/java/com/yj/earth/business/controller/RoleSourceController.java" root0="SKIP_INSPECTION" />
<setting file="file://$PROJECT_DIR$/src/main/java/com/yj/earth/business/controller/SourceController.java" root0="SKIP_INSPECTION" />
<setting file="file://$PROJECT_DIR$/src/main/java/com/yj/earth/business/controller/UserController.java" root0="SKIP_INSPECTION" />
<setting file="file://$PROJECT_DIR$/src/main/java/com/yj/earth/business/service/impl/SourceServiceImpl.java" root0="SKIP_INSPECTION" />
<setting file="file://$PROJECT_DIR$/src/main/java/com/yj/earth/common/config/SourceTypeConfig.java" root0="SKIP_INSPECTION" />
<setting file="file://$PROJECT_DIR$/src/main/java/com/yj/earth/common/constant/GlobalConstant.java" root0="SKIP_INSPECTION" />
<setting file="file://$PROJECT_DIR$/src/main/java/com/yj/earth/common/service/ServerInitService.java" root0="SKIP_INSPECTION" />
<setting file="file://$PROJECT_DIR$/src/main/java/com/yj/earth/common/util/SdkUtil.java" root0="SKIP_INSPECTION" />
<setting file="file://$PROJECT_DIR$/src/main/java/com/yj/earth/datasource/DatabaseManager.java" root0="SKIP_INSPECTION" />
<setting file="file://$PROJECT_DIR$/src/main/java/com/yj/earth/datasource/MysqlDataSourceConfig.java" root0="SKIP_INSPECTION" />
<setting file="file://$PROJECT_DIR$/src/main/java/com/yj/earth/params/BillboardObject.java" root0="SKIP_INSPECTION" />
<setting file="file://$PROJECT_DIR$/src/main/java/com/yj/earth/params/PolylineObject.java" root0="SKIP_INSPECTION" />
<setting file="file://$PROJECT_DIR$/src/main/java/com/yj/earth/sdk/CltController.java" root0="SKIP_INSPECTION" />
<setting file="file://$PROJECT_DIR$/src/main/java/com/yj/earth/sdk/PakController.java" root0="SKIP_INSPECTION" />
<setting file="file://$PROJECT_DIR$/src/main/resources/application.yml" root0="SKIP_INSPECTION" />
</component>
<component name="MarkdownSettingsMigration">
<option name="stateVersion" value="1" />
</component>
<component name="MavenImportPreferences">
<option name="generalSettings">
<MavenGeneralSettings>
<option name="localRepository" value="D:\study\Repository" />
<option name="mavenHome" value="D:\study\Maven\apache-maven-3.9.6" />
<option name="userSettingsFile" value="D:\study\Maven\apache-maven-3.9.6\conf\settings.xml" />
</MavenGeneralSettings>
</option>
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 7
}</component>
<component name="ProjectId" id="31lAoxTxxk0EgrlqLOJXEphma7d" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"Application.AuthValidator.executor": "Run",
"Application.CodeGenerator.executor": "Run",
"Application.CodeUtil.executor": "Run",
"Application.ImageResizer.executor": "Run",
"Application.PositionUtil.executor": "Run",
"Application.ResourceMonitor.executor": "Run",
"Application.ServerUniqueIdUtil.executor": "Run",
"Application.SqliteTableCreator.executor": "Run",
"Application.SystemUniqueIdGenerator.executor": "Run",
"Application.YjearthCacheManager.executor": "Run",
"Application.com.yj.earth.common.util.CodeGenerator.executor": "Run",
"Application.com.zzx.djl.utils.CodeUtil.executor": "Run",
"DefaultHtmlFileTemplate": "HTML File",
"Maven.yjearth [clean].executor": "Run",
"Maven.yjearth [package].executor": "Run",
"RequestMappingsPanelOrder0": "0",
"RequestMappingsPanelOrder1": "1",
"RequestMappingsPanelWidth0": "75",
"RequestMappingsPanelWidth1": "75",
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"Spring Boot.ServerApp.executor": "Run",
"kotlin-language-version-configured": "true",
"last_opened_file_path": "E:/yjearth/sdk",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"project.structure.last.edited": "Modules",
"project.structure.proportion": "0.0",
"project.structure.side.proportion": "0.0",
"settings.editor.selected.configurable": "MavenSettings",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
<component name="ReactorSettings">
<option name="notificationShown" value="true" />
</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="E:\yjearth\sdk" />
<recent name="E:\yjearth\src\main\java\com\yj\earth\auth" />
<recent name="E:\yjearth\src\main\java\com\yj\earth\params" />
<recent name="E:\yjearth" />
<recent name="E:\yjearth\src\main\java\com\yj\earth\model" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="E:\yjearth\src\main\java\com\yj\earth\vo" />
<recent name="E:\yjearth\sdk" />
</key>
<key name="CopyClassDialog.RECENTS_KEY">
<recent name="com.yj.earth.dto.user" />
<recent name="com.yj.earth.sdk" />
<recent name="com.yj.earth.business.controller" />
<recent name="com.yj.earth.design" />
<recent name="com.yj.earth.common.config" />
</key>
</component>
<component name="RunManager" selected="Spring Boot.ServerApp">
<configuration name="AuthValidator" type="Application" factoryName="Application" temporary="true" nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="com.yj.earth.auth.AuthValidator" />
<module name="yjearth" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="com.yj.earth.auth.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<configuration name="ServerApp" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" temporary="true" nameIsGenerated="true">
<module name="yjearth" />
<option name="SPRING_BOOT_MAIN_CLASS" value="com.yj.earth.ServerApp" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="com.yj.earth.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<list>
<item itemvalue="Application.AuthValidator" />
<item itemvalue="Spring Boot.ServerApp" />
</list>
<recent_temporary>
<list>
<item itemvalue="Spring Boot.ServerApp" />
<item itemvalue="Application.AuthValidator" />
</list>
</recent_temporary>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="1ba8c79d-5980-42c3-ba31-837b4b96bb22" name="Changes" comment="" />
<created>1756088463625</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1756088463625</updated>
<workItem from="1756088464610" duration="36122000" />
<workItem from="1756186510770" duration="3364000" />
<workItem from="1756190749415" duration="20215000" />
<workItem from="1756264811990" duration="16689000" />
<workItem from="1756290680677" duration="26000" />
<workItem from="1756343883318" duration="12487000" />
<workItem from="1756433614798" duration="33975000" />
<workItem from="1756710190316" duration="22200000" />
<workItem from="1756887638663" duration="1340000" />
<workItem from="1756951924235" duration="11288000" />
<workItem from="1757300079963" duration="14440000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
</project>

768
geo.html Normal file
View File

@ -0,0 +1,768 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>路径规划系统</title>
<!-- 仅保留必要CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<script src="https://cdn.jsdelivr.net/npm/axios@1.6.8/dist/axios.min.js"></script>
<!-- Tailwind配置 -->
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#165DFF',
success: '#00B42A',
danger: '#F53F3F',
warning: '#FF7D00',
neutral: '#F5F7FA',
'neutral-dark': '#4E5969',
},
fontFamily: {
inter: ['Inter', 'system-ui', 'sans-serif'],
},
}
}
}
</script>
<!-- 自定义工具类 -->
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
content-visibility: auto;
}
.map-height {
height: 100vh; /* 调整为占满视口高度 */
}
.sidebar-height {
height: 100vh; /* 调整为占满视口高度 */
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.custom-marker .fa {
font-size: 14px;
}
.input-error {
@apply border-danger focus:ring-danger/50 focus:border-danger;
}
.btn-disabled {
@apply bg-gray-300 text-gray-500 cursor-not-allowed hover:bg-gray-300;
}
}
</style>
</head>
<body class="font-inter bg-gray-50 text-gray-800 antialiased m-0">
<!-- 主内容区直接顶到顶部、删除原header -->
<main class="flex flex-col md:flex-row">
<!-- 左侧控制面板 -->
<aside class="w-full md:w-96 bg-white shadow-sm z-10 md:sidebar-height overflow-y-auto scrollbar-hide transition-all">
<div class="p-4 space-y-6">
<!-- 地图加载区域 -->
<div class="p-4 border border-gray-100 rounded-lg bg-neutral shadow-sm">
<h2 class="text-lg font-semibold mb-3 flex items-center text-neutral-dark">
<i class="fa fa-map text-primary mr-2"></i>地图管理
</h2>
<div class="flex items-center space-x-2 mb-2">
<input type="file" id="mapFile" accept=".pbf" class="hidden" multiple="false"/>
<button id="selectFileBtn"
class="flex-1 px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded text-sm font-medium transition-colors duration-200">
选择PBF文件
</button>
<button id="loadMapBtn"
class="px-4 py-2 bg-primary hover:bg-primary/90 text-white rounded text-sm font-medium transition-colors duration-200">
加载地图
</button>
</div>
<div id="fileInfo" class="mt-1 text-sm hidden"></div>
<div id="loadProgress" class="mt-1 text-sm text-primary hidden flex items-center">
<i class="fa fa-spinner fa-spin mr-1.5"></i>
<span id="progressText">正在处理...</span>
</div>
</div>
<!-- 路径规划参数 -->
<div class="space-y-4">
<h2 class="text-lg font-semibold flex items-center text-neutral-dark">
<i class="fa fa-road text-primary mr-2"></i>路径参数
</h2>
<!-- 起点移除默认value、默认无数据 -->
<div class="space-y-1">
<label class="block text-sm font-medium text-gray-700">起点 <span class="text-danger">*</span></label>
<div class="flex space-x-2">
<div class="flex-1 space-y-0.5">
<input type="text" id="startLat" placeholder="纬度"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary text-sm transition-all"
maxlength="10">
<span id="startLatError" class="text-danger text-xs hidden">请输入有效纬度</span>
</div>
<div class="flex-1 space-y-0.5">
<input type="text" id="startLng" placeholder="经度"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary text-sm transition-all"
maxlength="10">
<span id="startLngError" class="text-danger text-xs hidden">请输入有效经度</span>
</div>
<button id="setStartBtn" class="p-2 bg-gray-100 hover:bg-gray-200 rounded transition-colors duration-200"
title="在地图上选择起点">
<i class="fa fa-map-marker text-danger"></i>
</button>
</div>
</div>
<!-- 终点 -->
<div class="space-y-1">
<label class="block text-sm font-medium text-gray-700">终点 <span class="text-danger">*</span></label>
<div class="flex space-x-2">
<div class="flex-1 space-y-0.5">
<input type="text" id="endLat" placeholder="纬度"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary text-sm transition-all"
maxlength="10">
<span id="endLatError" class="text-danger text-xs hidden">请输入有效纬度</span>
</div>
<div class="flex-1 space-y-0.5">
<input type="text" id="endLng" placeholder="经度"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary text-sm transition-all"
maxlength="10">
<span id="endLngError" class="text-danger text-xs hidden">请输入有效经度</span>
</div>
<button id="setEndBtn" class="p-2 bg-gray-100 hover:bg-gray-200 rounded transition-colors duration-200"
title="在地图上选择终点">
<i class="fa fa-flag text-success"></i>
</button>
</div>
</div>
<!-- 途经点 -->
<div class="space-y-1">
<div class="flex justify-between items-center">
<label class="block text-sm font-medium text-gray-700">途经点</label>
<button id="addWaypointBtn"
class="text-xs px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded transition-colors duration-200 flex items-center">
<i class="fa fa-plus mr-1"></i>添加
</button>
</div>
<div id="waypointsContainer" class="space-y-2">
<!-- 途经点动态添加 -->
</div>
</div>
<!-- 交通方式 -->
<div class="space-y-1">
<label class="block text-sm font-medium text-gray-700">交通方式 <span class="text-danger">*</span></label>
<select id="profile"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary text-sm transition-all bg-white">
<option value="car">驾车</option>
<option value="bike">骑行</option>
<option value="foot">步行</option>
</select>
</div>
<!-- 计算路径按钮 -->
<button id="calculateRouteBtn"
class="w-full py-2.5 bg-primary hover:bg-primary/90 text-white rounded-md font-medium transition-colors duration-200 flex items-center justify-center btn-disabled"
disabled>
<i class="fa fa-calculator mr-2"></i>请先加载地图
</button>
</div>
<!-- 结果展示区域 -->
<div id="resultContainer" class="p-4 border border-gray-100 rounded-lg bg-neutral shadow-sm hidden">
<h2 class="text-lg font-semibold mb-3 flex items-center text-neutral-dark">
<i class="fa fa-check-circle text-success mr-2"></i>路径结果
</h2>
<div class="grid grid-cols-2 gap-4 mb-4">
<div class="bg-white p-3 rounded shadow-sm border border-gray-100 hover:shadow-md transition-shadow">
<div class="text-xs text-gray-500 mb-1">总距离</div>
<div id="distanceResult" class="text-lg font-semibold text-gray-800">-</div>
</div>
<div class="bg-white p-3 rounded shadow-sm border border-gray-100 hover:shadow-md transition-shadow">
<div class="text-xs text-gray-500 mb-1">预计时间</div>
<div id="timeResult" class="text-lg font-semibold text-gray-800">-</div>
</div>
</div>
<div class="grid grid-cols-2 gap-2">
<button id="clearRouteBtn"
class="py-2 border border-gray-300 bg-white hover:bg-gray-50 text-gray-700 rounded-md text-sm font-medium transition-colors duration-200">
<i class="fa fa-trash mr-1"></i>清除路径
</button>
<button id="clearAllBtn"
class="py-2 border border-gray-300 bg-white hover:bg-gray-50 text-gray-700 rounded-md text-sm font-medium transition-colors duration-200">
<i class="fa fa-refresh mr-1"></i>清空所有
</button>
</div>
</div>
</div>
</aside>
<!-- 右侧地图区域 -->
<section class="flex-1 relative">
<div id="map" class="w-full map-height z-0"></div>
<!-- 地图控件 -->
<div class="absolute top-4 right-4 z-10 flex flex-col space-y-2">
<button id="zoomInBtn"
class="w-10 h-10 bg-white rounded-full shadow-md flex items-center justify-center hover:bg-gray-100 transition-colors duration-200"
title="放大">
<i class="fa fa-plus"></i>
</button>
<button id="zoomOutBtn"
class="w-10 h-10 bg-white rounded-full shadow-md flex items-center justify-center hover:bg-gray-100 transition-colors duration-200"
title="缩小">
<i class="fa fa-minus"></i>
</button>
<button id="centerMapBtn"
class="w-10 h-10 bg-white rounded-full shadow-md flex items-center justify-center hover:bg-gray-100 transition-colors duration-200"
title="重置中心(成都)">
<i class="fa fa-crosshairs"></i>
</button>
</div>
<!-- 地图操作提示(移动端) -->
<div class="absolute bottom-4 left-4 z-10 md:hidden bg-white/90 px-3 py-2 rounded-full text-xs text-neutral-dark shadow-md">
<i class="fa fa-hand-pointer-o text-primary mr-1"></i>点击地图设起点/终点
</div>
</section>
</main>
<script>
// 大整数转字符串处理(保留原逻辑)
axios.defaults.transformResponse = [
function(data) {
if (typeof data !== 'string') return data;
const bigIntRegex = /(\s*"[^"]*"\s*:\s*)(\d{16,})(\s*)/g;
return data.replace(bigIntRegex, (match, keyPart, bigInt, endPart) => {
return `${keyPart}"${bigInt}"${endPart}`;
});
},
function(parsedData) {
try {
return JSON.parse(parsedData);
} catch (e) {
return parsedData;
}
}
];
// 全局变量
let map;
let startMarker = null;
let endMarker = null;
let waypointMarkers = [];
let routeLine = null;
let waypointCount = 0;
const API_BASE_URL = "http://127.0.0.1:8848";
const DEFAULT_CENTER = { lat: 30.6570, lng: 104.0650 }; // 成都默认坐标
// 地图初始化
function initMap() {
map = L.map('map', {
zoomControl: false,
attributionControl: true,
minZoom: 5,
maxZoom: 18
}).setView([DEFAULT_CENTER.lat, DEFAULT_CENTER.lng], 12);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
tileSize: 256,
zoomOffset: 0
}).addTo(map);
map.on('click', handleMapClick);
}
// 表单验证
function validateCoord(value, type) {
const num = parseFloat(value);
if (isNaN(num)) return false;
return type === 'lat' ? (num >= -90 && num <= 90) : (num >= -180 && num <= 180);
}
function toggleInputError(inputId, errorId, show, inputEl = null, errorEl = null) {
inputEl = inputEl || document.getElementById(inputId);
errorEl = errorEl || document.getElementById(errorId);
if (show) {
inputEl.classList.add('input-error');
errorEl.classList.remove('hidden');
} else {
inputEl.classList.remove('input-error');
errorEl.classList.add('hidden');
}
}
// 绑定坐标验证
function bindCoordValidation() {
// 起点验证
document.getElementById('startLat').addEventListener('input', (e) => {
const isValid = validateCoord(e.target.value, 'lat');
toggleInputError('startLat', 'startLatError', !isValid);
});
document.getElementById('startLng').addEventListener('input', (e) => {
const isValid = validateCoord(e.target.value, 'lng');
toggleInputError('startLng', 'startLngError', !isValid);
});
// 终点验证
document.getElementById('endLat').addEventListener('input', (e) => {
const isValid = validateCoord(e.target.value, 'lat');
toggleInputError('endLat', 'endLatError', !isValid);
});
document.getElementById('endLng').addEventListener('input', (e) => {
const isValid = validateCoord(e.target.value, 'lng');
toggleInputError('endLng', 'endLngError', !isValid);
});
}
// 标记管理
function setStartPoint(lat, lng) {
const latEl = document.getElementById('startLat');
const lngEl = document.getElementById('startLng');
latEl.value = lat.toFixed(6);
lngEl.value = lng.toFixed(6);
latEl.dispatchEvent(new Event('input'));
lngEl.dispatchEvent(new Event('input'));
if (startMarker) {
startMarker.setLatLng([lat, lng]);
} else {
startMarker = L.marker([lat, lng], {
icon: L.divIcon({
className: 'custom-marker',
html: '<div class="w-6 h-6 bg-danger rounded-full flex items-center justify-center text-white shadow-md"><i class="fa fa-map-marker"></i></div>',
iconSize: [30, 30],
iconAnchor: [15, 30]
}),
draggable: true,
riseOnHover: true
}).addTo(map);
startMarker.on('dragend', (e) => {
const { lat, lng } = e.target.getLatLng();
setStartPoint(lat, lng);
});
}
}
function setEndPoint(lat, lng) {
const latEl = document.getElementById('endLat');
const lngEl = document.getElementById('endLng');
latEl.value = lat.toFixed(6);
lngEl.value = lng.toFixed(6);
latEl.dispatchEvent(new Event('input'));
lngEl.dispatchEvent(new Event('input'));
if (endMarker) {
endMarker.setLatLng([lat, lng]);
} else {
endMarker = L.marker([lat, lng], {
icon: L.divIcon({
className: 'custom-marker',
html: '<div class="w-6 h-6 bg-success rounded-full flex items-center justify-center text-white shadow-md"><i class="fa fa-flag"></i></div>',
iconSize: [30, 30],
iconAnchor: [15, 30]
}),
draggable: true,
riseOnHover: true
}).addTo(map);
endMarker.on('dragend', (e) => {
const { lat, lng } = e.target.getLatLng();
setEndPoint(lat, lng);
});
}
}
function addWaypoint(lat = '', lng = '') {
waypointCount++;
const container = document.getElementById('waypointsContainer');
const waypointDiv = document.createElement('div');
waypointDiv.className = 'flex space-x-2 waypoint-item';
waypointDiv.dataset.id = waypointCount;
waypointDiv.innerHTML = `
<div class="flex-1 space-y-0.5">
<input type="text" class="waypoint-lat w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary text-sm transition-all"
placeholder="纬度" value="${lat}" maxlength="10">
<span class="waypoint-lat-error text-danger text-xs hidden">请输入有效纬度</span>
</div>
<div class="flex-1 space-y-0.5">
<input type="text" class="waypoint-lng w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary text-sm transition-all"
placeholder="经度" value="${lng}" maxlength="10">
<span class="waypoint-lng-error text-danger text-xs hidden">请输入有效经度</span>
</div>
<button class="set-waypoint-btn p-2 bg-gray-100 hover:bg-gray-200 rounded transition-colors duration-200" title="在地图上选择">
<i class="fa fa-map-pin text-primary"></i>
</button>
<button class="remove-waypoint-btn p-2 bg-gray-100 hover:bg-gray-200 rounded transition-colors duration-200" title="删除途经点">
<i class="fa fa-times text-gray-500"></i>
</button>
`;
container.appendChild(waypointDiv);
const latInput = waypointDiv.querySelector('.waypoint-lat');
const lngInput = waypointDiv.querySelector('.waypoint-lng');
const latError = waypointDiv.querySelector('.waypoint-lat-error');
const lngError = waypointDiv.querySelector('.waypoint-lng-error');
latInput.addEventListener('input', (e) => {
const isValid = validateCoord(e.target.value, 'lat');
toggleInputError(null, null, !isValid, latInput, latError);
});
lngInput.addEventListener('input', (e) => {
const isValid = validateCoord(e.target.value, 'lng');
toggleInputError(null, null, !isValid, lngInput, lngError);
});
// 途经点地图选点
waypointDiv.querySelector('.set-waypoint-btn').addEventListener('click', () => {
const id = parseInt(waypointDiv.dataset.id);
map.once('click', (e) => {
const { lat, lng } = e.latlng;
latInput.value = lat.toFixed(6);
lngInput.value = lng.toFixed(6);
latInput.dispatchEvent(new Event('input'));
lngInput.dispatchEvent(new Event('input'));
if (waypointMarkers[id]) {
waypointMarkers[id].setLatLng([lat, lng]);
} else {
waypointMarkers[id] = L.marker([lat, lng], {
icon: L.divIcon({
className: 'custom-marker',
html: `<div class="w-5 h-5 bg-primary rounded-full flex items-center justify-center text-white text-xs shadow-md">${id}</div>`,
iconSize: [25, 25],
iconAnchor: [12, 25]
}),
draggable: true,
riseOnHover: true
}).addTo(map);
waypointMarkers[id].on('dragend', (e) => {
const { lat, lng } = e.target.getLatLng();
latInput.value = lat.toFixed(6);
lngInput.value = lng.toFixed(6);
latInput.dispatchEvent(new Event('input'));
lngInput.dispatchEvent(new Event('input'));
});
}
});
});
// 删除途经点
waypointDiv.querySelector('.remove-waypoint-btn').addEventListener('click', () => {
const id = parseInt(waypointDiv.dataset.id);
if (waypointMarkers[id]) {
map.removeLayer(waypointMarkers[id]);
waypointMarkers[id] = null;
}
waypointDiv.remove();
});
}
// 地图点击处理
function handleMapClick(e) {
const { lat, lng } = e.latlng;
const startLat = document.getElementById('startLat').value;
const endLat = document.getElementById('endLat').value;
// 先设起点(空则设起点)→ 再设终点
if (!startLat.trim()) {
setStartPoint(lat, lng);
} else if (!endLat.trim()) {
setEndPoint(lat, lng);
}
}
// 路径操作
function clearRoute() {
if (routeLine) {
map.removeLayer(routeLine);
routeLine = null;
}
document.getElementById('resultContainer').classList.add('hidden');
}
// 清空所有:起点重置为空(而非默认坐标)
function clearAll() {
clearRoute();
// 清除起点(重置为空)
if (startMarker) {
map.removeLayer(startMarker);
startMarker = null;
}
document.getElementById('startLat').value = '';
document.getElementById('startLng').value = '';
toggleInputError('startLat', 'startLatError', false);
toggleInputError('startLng', 'startLngError', false);
// 清除终点
if (endMarker) {
map.removeLayer(endMarker);
endMarker = null;
}
document.getElementById('endLat').value = '';
document.getElementById('endLng').value = '';
toggleInputError('endLat', 'endLatError', false);
toggleInputError('endLng', 'endLngError', false);
// 清除途经点
waypointMarkers.forEach(marker => {
if (marker) map.removeLayer(marker);
});
waypointMarkers = [];
document.getElementById('waypointsContainer').innerHTML = '';
waypointCount = 0;
addWaypoint(); // 重置默认空途经点
}
// 地图加载进度
function updateLoadProgress(show, text = '正在处理...') {
const progressEl = document.getElementById('loadProgress');
const textEl = document.getElementById('progressText');
if (show) {
progressEl.classList.remove('hidden');
textEl.textContent = text;
} else {
progressEl.classList.add('hidden');
}
}
// 上传PBF文件
async function uploadPbfFile(file) {
try {
const formData = new FormData();
formData.append('files', file);
updateLoadProgress(true, '正在上传地图文件...');
const response = await axios.post(`${API_BASE_URL}/fileInfo/upload`, formData, {
onUploadProgress: (progressEvent) => {
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100);
if (percent < 100) {
updateLoadProgress(true, `上传中... ${percent}%`);
}
}
});
if (response.data.code !== 200 || !response.data.data || !response.data.data[0]) {
throw new Error(`上传失败:${response.data.message || '未知错误'}`);
}
const fileId = response.data.data[0].id;
updateLoadProgress(true, `上传成功ID: ${fileId})、正在加载地图...`);
return fileId;
} catch (error) {
const errorMsg = error.response?.data?.message || error.message || '上传异常';
updateLoadProgress(false);
alert(`⚠️ 上传失败:${errorMsg}`);
throw error;
}
}
// 加载地图
async function loadMapByFileId(fileId) {
try {
const formData = new FormData();
formData.append('fileId', fileId);
const response = await axios.post(`${API_BASE_URL}/graphhopper/loadMap`, formData);
if (response.data.code !== 200) {
throw new Error(`加载失败:${response.data.message || '接口返回异常'}`);
}
updateLoadProgress(false);
const calcBtn = document.getElementById('calculateRouteBtn');
calcBtn.disabled = false;
calcBtn.classList.remove('btn-disabled');
calcBtn.innerHTML = '<i class="fa fa-calculator mr-2"></i>计算路径';
return true;
} catch (error) {
const errorMsg = error.response?.data?.message || error.message || '加载异常';
updateLoadProgress(false);
alert(`⚠️ 地图加载失败:${errorMsg}`);
throw error;
}
}
// 计算路径
async function calculateRoute() {
const startLat = parseFloat(document.getElementById('startLat').value);
const startLng = parseFloat(document.getElementById('startLng').value);
const endLat = parseFloat(document.getElementById('endLat').value);
const endLng = parseFloat(document.getElementById('endLng').value);
const profile = document.getElementById('profile').value;
// 基础验证
if ([startLat, startLng, endLat, endLng].some(isNaN)) {
alert('⚠️ 请确保起点/终点坐标为有效数字');
return;
}
if (!validateCoord(startLat, 'lat') || !validateCoord(startLng, 'lng')) {
alert('⚠️ 起点坐标超出有效范围(纬度-90~90、经度-180~180');
return;
}
if (!validateCoord(endLat, 'lat') || !validateCoord(endLng, 'lng')) {
alert('⚠️ 终点坐标超出有效范围(纬度-90~90、经度-180~180');
return;
}
// 处理途经点
const waypoints = [];
document.querySelectorAll('.waypoint-item').forEach(item => {
const lat = parseFloat(item.querySelector('.waypoint-lat').value);
const lng = parseFloat(item.querySelector('.waypoint-lng').value);
if (!isNaN(lat) && !isNaN(lng) && validateCoord(lat, 'lat') && validateCoord(lng, 'lng')) {
waypoints.push({ lat, lng });
}
});
// 发起请求
const calcBtn = document.getElementById('calculateRouteBtn');
calcBtn.disabled = true;
calcBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i>计算中...';
try {
const response = await axios.post(
`${API_BASE_URL}/graphhopper/route`,
{ startLat, startLng, endLat, endLng, profile, waypoints },
{ headers: { 'Content-Type': 'application/json' } }
);
if (response.data.code !== 200 || !response.data.data) {
throw new Error(`计算失败:${response.data.message || '接口返回异常'}`);
}
handleRouteResponse(response.data.data);
} catch (error) {
const errorMsg = error.response?.data?.message || error.message || '计算异常';
alert(`⚠️ 路径计算失败:${errorMsg}`);
} finally {
calcBtn.disabled = false;
calcBtn.innerHTML = '<i class="fa fa-calculator mr-2"></i>计算路径';
}
}
// 处理路径结果
function handleRouteResponse(routeData) {
document.getElementById('distanceResult').textContent = `${routeData.distanceKm.toFixed(2)} 公里`;
document.getElementById('timeResult').textContent = `${routeData.timeMinutes} 分钟`;
document.getElementById('resultContainer').classList.remove('hidden');
if (routeLine) map.removeLayer(routeLine);
const latLngs = routeData.pathPoints.map(point => [point.lat, point.lng]);
const lineStyles = {
car: { color: '#165DFF', weight: 5, opacity: 0.8, dashArray: '' },
bike: { color: '#00B42A', weight: 4, opacity: 0.8, dashArray: '5,5' },
foot: { color: '#4b0c35', weight: 3, opacity: 0.8, dashArray: '2,2' }
};
routeLine = L.polyline(latLngs, lineStyles[document.getElementById('profile').value])
.addTo(map)
.bindPopup(`<div class="text-sm"><p>距离:${routeData.distanceKm.toFixed(2)} 公里</p><p>时间:${routeData.timeMinutes} 分钟</p></div>`);
map.fitBounds(routeLine.getBounds(), { padding: [50, 50], maxZoom: 14 });
}
// 事件绑定:移除起点按钮弹窗
function bindEvents() {
// 文件选择相关
document.getElementById('selectFileBtn').addEventListener('click', () => {
document.getElementById('mapFile').click();
});
document.getElementById('mapFile').addEventListener('change', (e) => {
const fileInfoEl = document.getElementById('fileInfo');
if (e.target.files.length === 0) {
fileInfoEl.classList.add('hidden');
return;
}
const file = e.target.files[0];
if (file.name.toLowerCase().endsWith('.pbf')) {
const fileSizeMB = (file.size / (1024 * 1024)).toFixed(2);
fileInfoEl.textContent = `已选择:${file.name}${fileSizeMB} MB`;
fileInfoEl.className = 'mt-1 text-sm text-gray-600';
} else {
fileInfoEl.textContent = '❌ 请选择PBF格式的地图文件';
fileInfoEl.className = 'mt-1 text-sm text-danger';
e.target.value = '';
}
});
document.getElementById('loadMapBtn').addEventListener('click', async () => {
const fileInput = document.getElementById('mapFile');
if (fileInput.files.length === 0) {
alert('⚠️ 请先选择PBF格式地图文件');
return;
}
try {
const file = fileInput.files[0];
const fileId = await uploadPbfFile(file);
await loadMapByFileId(fileId);
fileInput.value = '';
document.getElementById('fileInfo').classList.add('hidden');
} catch (error) {
console.error('地图加载失败:', error);
}
});
// 起点按钮:移除弹窗、仅保留地图选点逻辑
document.getElementById('setStartBtn').addEventListener('click', () => {
map.once('click', (e) => setStartPoint(e.latlng.lat, e.latlng.lng));
});
// 终点按钮保留弹窗、如需统一移除可删除alert
document.getElementById('setEndBtn').addEventListener('click', () => {
map.once('click', (e) => setEndPoint(e.latlng.lat, e.latlng.lng));
});
// 途经点/路径操作
document.getElementById('addWaypointBtn').addEventListener('click', () => addWaypoint());
document.getElementById('calculateRouteBtn').addEventListener('click', calculateRoute);
document.getElementById('clearRouteBtn').addEventListener('click', clearRoute);
document.getElementById('clearAllBtn').addEventListener('click', clearAll);
// 地图控件
document.getElementById('zoomInBtn').addEventListener('click', () => map.zoomIn());
document.getElementById('zoomOutBtn').addEventListener('click', () => map.zoomOut());
document.getElementById('centerMapBtn').addEventListener('click', () => {
map.setView([DEFAULT_CENTER.lat, DEFAULT_CENTER.lng], 12);
});
// 表单验证绑定
bindCoordValidation();
}
// 应用初始化
function initApp() {
initMap();
bindEvents();
addWaypoint(); // 默认添加1个空途经点
}
// 页面加载完成后初始化
window.addEventListener('load', initApp);
</script>
</body>
</html>

438
index.html Normal file
View File

@ -0,0 +1,438 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cesium 多资源整合加载平台</title>
<!-- 引入Cesium库使用1.116稳定版本) -->
<script src="https://cesium.com/downloads/cesiumjs/releases/1.116/Build/Cesium/Cesium.js"></script>
<link href="https://cesium.com/downloads/cesiumjs/releases/1.116/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
<style>
/* 基础样式重置 */
html, body, #cesiumContainer {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
/* 控制面板样式 */
.control-panel {
position: absolute;
top: 20px;
left: 20px;
background: rgba(255, 255, 255, 0.95);
padding: 20px;
border-radius: 8px;
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.15);
z-index: 100;
width: 420px;
}
/* 表单标题 */
.panel-title {
margin: 0 0 15px;
font-size: 18px;
color: #2c3e50;
border-bottom: 1px solid #eee;
padding-bottom: 8px;
}
/* 表单分组样式 */
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: 500;
color: #34495e;
}
/* 输入框、选择框样式 */
.form-control {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
transition: border-color 0.3s;
}
.form-control:focus {
outline: none;
border-color: #0070f3;
box-shadow: 0 0 0 2px rgba(0, 112, 243, 0.1);
}
/* 经纬度输入行布局 */
.coord-group {
display: flex;
gap: 10px;
}
.coord-group .form-group {
flex: 1;
}
/* 按钮样式 */
.btn-group {
display: flex;
gap: 10px;
margin-top: 10px;
}
.btn {
padding: 10px 16px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.3s;
}
.btn-primary {
background-color: #0070f3;
color: white;
}
.btn-primary:hover {
background-color: #0051aa;
}
.btn-locate {
background-color: #22c55e;
color: white;
}
.btn-locate:hover {
background-color: #16a34a;
}
.btn-clear {
background-color: #f37000;
color: white;
}
.btn-clear:hover {
background-color: #d96000;
}
/* 状态提示样式 */
.status {
margin-top: 12px;
padding: 10px;
border-radius: 4px;
font-size: 14px;
display: none; /* 默认隐藏 */
}
.status-success {
background-color: #e8f5e9;
color: #2e7d32;
display: block; /* 成功时显示 */
}
.status-error {
background-color: #fdecea;
color: #d32f2f;
display: block; /* 错误时显示 */
}
/* 鼠标经纬度提示框样式 */
.latlng-tooltip {
position: absolute;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 6px 10px;
border-radius: 4px;
font-size: 12px;
pointer-events: none; /* 避免遮挡鼠标事件 */
z-index: 1000; /* 确保在最上层显示 */
opacity: 0; /* 默认透明 */
transition: opacity 0.2s; /* 淡入淡出效果 */
white-space: nowrap; /* 防止文本换行 */
}
</style>
</head>
<body>
<!-- Cesium 容器 -->
<div id="cesiumContainer"></div>
<!-- 鼠标经纬度提示框 -->
<div id="latlngTooltip" class="latlng-tooltip"></div>
<!-- 控制表单面板 -->
<div class="control-panel">
<h3 class="panel-title">Cesium 资源加载与定位</h3>
<!-- 基础地址输入项 -->
<div class="form-group">
<label for="baseUrl">基础地址(服务器根地址)</label>
<input type="text" id="baseUrl" class="form-control"
placeholder="例: http://192.168.110.25:8848"
value="http://192.168.110.25:8848">
</div>
<!-- 资源地址输入 -->
<div class="form-group">
<label for="resourceUrl">资源相对路径</label>
<input type="text" id="resourceUrl" class="form-control"
placeholder="例: 1.倾斜模型: data/clt/.../tileset.json 2.高程: terrain_pak 3.瓦片: tiles/{z}/{x}/{y}.png"
value="/data/pak/8870a7a573dc621d7347457a5497df3b/{z}/{x}/{y}.png">
</div>
<!-- 经纬度定位输入 -->
<div class="coord-group">
<div class="form-group">
<label for="longitude">经度</label>
<input type="text" id="longitude" class="form-control"
placeholder="范围: -180 ~ 180" value="106.253200504443">
</div>
<div class="form-group">
<label for="latitude">纬度</label>
<input type="text" id="latitude" class="form-control"
placeholder="范围: -90 ~ 90" value="29.8500521523625">
</div>
</div>
<!-- 操作按钮组 -->
<div class="btn-group">
<button id="loadBtn" class="btn btn-primary">加载资源</button>
<button id="locateBtn" class="btn btn-locate">定位到经纬度</button>
<button id="clearBtn" class="btn btn-clear">清除所有资源</button>
</div>
<!-- 状态提示 -->
<div id="status" class="status"></div>
</div>
<script>
// 1. 初始化Cesium核心配置
Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI5MGU0NGMwYS00ZDBkLTQzMDItYjc5Zi0zYTM1NDcwZGVjMmEiLCJpZCI6MTU1MTk5LCJpYXQiOjE2ODk4MTgzNTF9.V_eZ5KlAI9qmxsDivT6pMC3Pq6qLk0mXpBoe5C0Mm4g';
// 2. 创建Cesium Viewer基础配置
const viewer = new Cesium.Viewer('cesiumContainer', {
animation: false,
timeline: false,
vrButton: false,
infoBox: false,
selectionIndicator: false,
homeButton: true,
sceneModePicker: true,
navigationHelpButton: true,
fullscreenButton: true,
imageryProvider: false
});
// 隐藏Cesium版权信息
viewer._cesiumWidget._creditContainer.style.display = 'none';
// 3. 全局变量
let loadedResource = { type: null, instance: null };
const statusEl = document.getElementById('status');
const latlngTooltip = document.getElementById('latlngTooltip');
// 4. 工具函数: 拼接资源URL
function getFullResourceUrl() {
const baseUrl = document.getElementById('baseUrl').value.trim();
const resourceUrl = document.getElementById('resourceUrl').value.trim();
if (!resourceUrl) return '';
if (!baseUrl) return resourceUrl;
const baseUrlWithSlash = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
const cleanResourceUrl = resourceUrl.startsWith('/') ? resourceUrl.slice(1) : resourceUrl;
return `${baseUrlWithSlash}${cleanResourceUrl}`;
}
// 5. 工具函数: 识别资源类型
function getResourceTypeByUrl(fullUrl) {
const lowerUrl = fullUrl.toLowerCase();
if (lowerUrl.includes('.png') || lowerUrl.includes('.jpg')) return 'imagery';
if (lowerUrl.includes('tileset.json')) return 'tileset';
if (lowerUrl.includes('pak')) return 'terrain';
return null;
}
// 6. 状态提示函数
function showStatus(message, isSuccess = true) {
statusEl.textContent = message;
statusEl.className = 'status';
statusEl.classList.add(isSuccess ? 'status-success' : 'status-error');
if (isSuccess) {
setTimeout(() => {
statusEl.classList.remove('status-success');
statusEl.style.display = 'none';
}, 5000);
}
}
// 7. 清除资源函数
function clearLoadedResource() {
if (!loadedResource.instance) return;
switch (loadedResource.type) {
case 'tileset':
viewer.scene.primitives.remove(loadedResource.instance);
showStatus('倾斜模型已清除');
break;
case 'imagery':
viewer.imageryLayers.remove(loadedResource.instance);
showStatus('二维瓦片已清除');
break;
case 'terrain':
viewer.terrainProvider = new Cesium.EllipsoidTerrainProvider();
viewer.scene.globe.depthTestAgainstTerrain = false;
showStatus('高程模型已清除');
break;
}
loadedResource = { type: null, instance: null };
}
// 8. 资源加载函数
async function loadResource() {
const fullUrl = getFullResourceUrl();
if (!fullUrl) {
showStatus('请输入有效的资源相对路径', false);
return;
}
const resourceType = getResourceTypeByUrl(fullUrl);
if (!resourceType) {
showStatus('无法识别资源类型、请检查路径是否包含tileset.json/pak/.png/.jpg', false);
return;
}
clearLoadedResource();
try {
let instance;
switch (resourceType) {
case 'tileset':
showStatus('正在加载倾斜模型...');
instance = await Cesium.Cesium3DTileset.fromUrl(fullUrl);
viewer.scene.primitives.add(instance);
viewer.zoomTo(instance);
showStatus('倾斜模型加载成功');
break;
case 'imagery':
showStatus('正在加载二维瓦片...');
const fileExtension = fullUrl.toLowerCase().includes('.png') ? 'png' : 'jpg';
const provider = new Cesium.UrlTemplateImageryProvider({
url: fullUrl,
fileExtension: fileExtension,
minimumLevel: 0,
maximumLevel: 18,
credit: '自定义二维瓦片'
});
instance = viewer.imageryLayers.addImageryProvider(provider);
showStatus('二维瓦片加载成功');
break;
case 'terrain':
showStatus('正在加载高程模型...');
instance = await Cesium.CesiumTerrainProvider.fromUrl(fullUrl);
viewer.terrainProvider = instance;
viewer.scene.globe.depthTestAgainstTerrain = true;
showStatus('高程模型加载成功');
break;
}
loadedResource = { type: resourceType, instance: instance };
} catch (error) {
console.error('资源加载失败: ', error);
const errorMsg = error.message.includes('404') ? '资源地址不存在404' :
error.message.includes('CORS') ? '跨域访问被拒绝CORS' :
'服务器连接失败或资源格式错误';
showStatus(`资源加载失败: ${errorMsg}`, false);
}
}
// 9. 经纬度定位函数
function flyToCoordinate() {
const lng = parseFloat(document.getElementById('longitude').value.trim());
const lat = parseFloat(document.getElementById('latitude').value.trim());
if (isNaN(lng) || isNaN(lat) || lng < -180 || lng > 180 || lat < -90 || lat > 90) {
showStatus('请输入有效的经纬度(经度: -180~180、纬度: -90~90', false);
return;
}
viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(lng, lat, 10000),
duration: 2,
orientation: {
heading: Cesium.Math.toRadians(0),
pitch: Cesium.Math.toRadians(-30),
roll: 0
}
});
showStatus(`已定位到经纬度: (${lng.toFixed(4)}, ${lat.toFixed(4)})`);
}
// 10. 修复: 鼠标悬浮显示经纬度(支持地球+地形表面、避免NaN
function initLatlngTooltip() {
const canvas = viewer.scene.canvas;
// 鼠标移动事件
canvas.addEventListener('mousemove', (e) => {
// 1. 先尝试获取地形表面坐标(优先、因为加载高程后更准确)
const windowPosition = new Cesium.Cartesian2(e.clientX, e.clientY);
let cartographic = null;
// 方法1: 从地形表面获取坐标(适用于加载了高程模型的场景)
const ray = viewer.camera.getPickRay(windowPosition);
if (ray) {
const hitResult = viewer.scene.pickFromRay(ray);
if (hitResult && hitResult.position) {
cartographic = Cesium.Cartographic.fromCartesian(hitResult.position);
}
}
// 方法2: 如果地形获取失败、从椭球面获取(适用于无高程的场景)
if (!cartographic) {
cartographic = viewer.scene.camera.pickEllipsoid(windowPosition);
}
// 3. 计算并显示经纬度(增加异常处理)
if (cartographic && !isNaN(cartographic.longitude) && !isNaN(cartographic.latitude)) {
const longitude = Cesium.Math.toDegrees(cartographic.longitude).toFixed(6);
const latitude = Cesium.Math.toDegrees(cartographic.latitude).toFixed(6);
// 显示经纬度避免NaN
latlngTooltip.textContent = `经度: ${longitude} | 纬度: ${latitude}`;
// 调整提示框位置(防止超出屏幕)
const tooltipWidth = latlngTooltip.offsetWidth || 150; // 预估宽度
const left = e.clientX + 10 > window.innerWidth - tooltipWidth
? e.clientX - tooltipWidth - 10
: e.clientX + 10;
const top = e.clientY + 10 > window.innerHeight - 30
? e.clientY - 30
: e.clientY + 10;
latlngTooltip.style.left = `${left}px`;
latlngTooltip.style.top = `${top}px`;
latlngTooltip.style.opacity = '1';
} else {
// 非地球表面时隐藏提示框
latlngTooltip.style.opacity = '0';
}
});
// 鼠标离开画布时隐藏
canvas.addEventListener('mouseleave', () => {
latlngTooltip.style.opacity = '0';
});
}
// 11. 绑定按钮事件
document.getElementById('loadBtn').addEventListener('click', loadResource);
document.getElementById('locateBtn').addEventListener('click', flyToCoordinate);
document.getElementById('clearBtn').addEventListener('click', () => {
clearLoadedResource();
if (viewer.imageryLayers.length > 0) {
viewer.imageryLayers.removeAll();
showStatus('所有影像图层已清除');
}
});
// 12. 初始化鼠标经纬度提示
initLatlngTooltip();
</script>
</body>
</html>

177
pom.xml Normal file
View File

@ -0,0 +1,177 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 项目GAV -->
<groupId>com.yj.earth</groupId>
<artifactId>yjearth</artifactId>
<version>1.0.0</version>
<!-- SpringBoot3 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MybatisPlus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.9</version>
</dependency>
<!-- MybatisPlus 代码生成器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.9</version>
</dependency>
<!-- MybatisPlus 附加插件 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
<version>3.5.9</version>
</dependency>
<!-- 模板引擎依赖 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
<!-- Mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- SQlite -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
</dependency>
<!-- 德鲁伊数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- Hutool工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.20</version>
</dependency>
<!-- Swagger3 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.5.0</version>
</dependency>
<!-- WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 获取操作系统信息 -->
<dependency>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId>
<version>6.4.0</version>
</dependency>
<!-- OkHttp -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</dependency>
<!-- SaToken -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.44.0</version>
</dependency>
<!-- GraphHopper -->
<dependency>
<groupId>com.graphhopper</groupId>
<artifactId>graphhopper-core</artifactId>
<version>7.0</version>
</dependency>
<!-- 用于地理多边形空间判断 -->
<dependency>
<groupId>org.locationtech.jts</groupId>
<artifactId>jts-core</artifactId>
<version>1.19.0</version>
</dependency>
<!-- Spring AOP -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
</dependency>
<!-- AspectJ 织入器 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<!-- HttpClient5 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<finalName>yjearth</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,54 @@
package com.yj.earth;
import com.yj.earth.business.service.SourceService;
import com.yj.earth.common.config.ServerConfig;
import com.yj.earth.common.service.ServerInitService;
import com.yj.earth.common.util.SdkUtil;
import com.yj.earth.datasource.DatabaseManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import javax.annotation.Resource;
import java.io.IOException;
@Slf4j
@EnableAspectJAutoProxy
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class ServerApp implements CommandLineRunner {
@Resource
private SourceService sourceService;
@Resource
private ServerConfig serverConfig;
@Resource
private ServerInitService serverInitService;
public static void main(String[] args) throws IOException {
// 启动项目SDK服务
SdkUtil.startSdkIfConfigured();
// 初始化数据库相关操作
String activeDataSource = DatabaseManager.getActiveDataSource();
// 获取数据库类型
DatabaseManager.DatabaseType dbType = DatabaseManager.DatabaseType.valueOf(activeDataSource.toUpperCase());
// 初始化数据库
DatabaseManager.initDatabase(dbType);
// 启动应用服务
SpringApplication application = new SpringApplication(ServerApp.class);
// 允许循环引用
application.setAllowCircularReferences(true);
// 启动服务
application.run(args);
}
@Override
public void run(String... args) throws Exception {
// 初始化资源
serverInitService.init();
// 检查默认数据
serverInitService.checkDefaultData();
log.info("项目文档地址: {}", "http://" + serverConfig.getHost() + ":" + serverConfig.getPort() + "/doc.html");
}
}

View File

@ -0,0 +1,22 @@
package com.yj.earth.annotation;
import java.lang.annotation.*;
/**
* 返回结果加密注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EncryptResponse {
/**
* 加密密钥的配置键
*/
String keyProperty() default "encrypt.aes.key";
/**
* 加密算法模式
*/
String algorithm() default "AES/CBC/PKCS5Padding";
}

View File

@ -0,0 +1,34 @@
package com.yj.earth.annotation;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.PropertyWriter;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 用于标记需要在JSON序列化时排除的字段
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcludeField {
/**
* Jackson 序列化过滤器、用于过滤被 @ExcludeField 注解标记的字段
*/
class Filter extends SimpleBeanPropertyFilter {
@Override
public void serializeAsField(Object pojo, JsonGenerator jsonGenerator, SerializerProvider provider, PropertyWriter writer) throws Exception {
// 检查字段是否有 @ExcludeField 注解、如果有则不序列化该字段
if (writer.getAnnotation(ExcludeField.class) != null) {
return;
}
// 没有注解的字段正常序列化
super.serializeAsField(pojo, jsonGenerator, provider, writer);
}
}
}

View File

@ -0,0 +1,16 @@
package com.yj.earth.annotation;
import java.lang.annotation.*;
/**
* 角色访问控制注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RoleAccess {
/**
* 允许访问的角色名称数组
*/
String[] roleNames();
}

View File

@ -0,0 +1,9 @@
package com.yj.earth.annotation;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface SourceType {
String value();
}

View File

@ -0,0 +1,64 @@
package com.yj.earth.aspect;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yj.earth.annotation.EncryptResponse;
import com.yj.earth.common.util.AesEncryptUtil;
import com.yj.earth.common.util.ApiResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
/**
* 响应加密切面: 拦截 @EncryptResponse 注解的方法、对返回结果进行加密
*/
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class EncryptResponseAspect {
private final ObjectMapper objectMapper;
private final Environment environment;
/**
* 定义切点: 拦截所有被 @EncryptResponse 标记的方法
*/
@Pointcut("@annotation(encryptResponse)")
public void pointCut(EncryptResponse encryptResponse) {}
/**
* 环绕通知: 对方法返回结果进行加密处理
*/
@Around("pointCut(encryptResponse)")
public Object around(ProceedingJoinPoint joinPoint, EncryptResponse encryptResponse) throws Throwable {
// 执行原方法、获取返回结果
Object result = joinPoint.proceed();
// 从配置文件获取密钥
String key = environment.getProperty(encryptResponse.keyProperty());
if (key == null || key.isEmpty()) {
log.error("加密密钥未配置、keyProperty: {}", encryptResponse.keyProperty());
throw new RuntimeException("加密密钥未配置");
}
// 将返回结果转为JSON字符串
String jsonResult;
try {
jsonResult = objectMapper.writeValueAsString(result);
} catch (JsonProcessingException e) {
log.error("返回结果转JSON失败", e);
throw new RuntimeException("返回结果序列化失败");
}
// 执行加密
String encryptedResult = AesEncryptUtil.encrypt(jsonResult, key, encryptResponse.algorithm());
log.debug("接口返回结果已加密、原始长度: {}、加密后长度: {}", jsonResult.length(), encryptedResult.length());
return ApiResponse.success(encryptedResult);
}
}

View File

@ -0,0 +1,78 @@
package com.yj.earth.aspect;
import cn.dev33.satoken.stp.StpUtil;
import com.yj.earth.annotation.RoleAccess;
import com.yj.earth.business.domain.User;
import com.yj.earth.business.domain.Role;
import com.yj.earth.business.service.UserService;
import com.yj.earth.business.service.RoleService;
import com.yj.earth.common.util.ApiResponse;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Method;
/**
* 角色访问控制切面
*/
@Aspect
@Component
public class RoleAccessAspect {
@Resource
private UserService userService;
@Resource
private RoleService roleService;
/**
* 环绕通知、验证角色权限
*/
@Around("@annotation(com.yj.earth.annotation.RoleAccess)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取当前登录用户ID
if (!StpUtil.isLogin()) {
return ApiResponse.failure("请先登录");
}
String userId = StpUtil.getLoginIdAsString();
// 获取用户信息
User user = userService.getById(userId);
if (user == null) {
return ApiResponse.failure("用户不存在");
}
// 获取用户角色信息
Role role = roleService.getById(user.getRoleId());
if (role == null) {
return ApiResponse.failure("用户角色不存在");
}
// 获取注解信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RoleAccess roleAccess = method.getAnnotation(RoleAccess.class);
String[] allowedRoles = roleAccess.roleNames();
// 验证角色是否有权限
boolean hasPermission = false;
for (String roleName : allowedRoles) {
if (roleName.equals(role.getRoleName())) {
hasPermission = true;
break;
}
}
if (!hasPermission) {
return ApiResponse.failure("没有访问权限、需要角色: " + String.join(",", allowedRoles));
}
// 有权限、执行原方法
return joinPoint.proceed();
}
}

View File

@ -0,0 +1,74 @@
package com.yj.earth.auth;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yj.earth.common.util.ServerUniqueIdUtil;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Calendar;
import java.util.Date;
/**
* 授权生成工具类
*/
public class AuthGenerator {
// AES加密密钥(16位)
private static final String AES_KEY = "7AJD6H5AGHY6SJU7";
// Jackson JSON处理器
private static final ObjectMapper objectMapper = new ObjectMapper();
/**
* 生成授权信息
*
* @param versionType 版本类型
* @param maxResourceCount 最大资源数量
* @param authDays 授权天数
* @param serverHardwareMd5 服务器硬件信息MD5
* @return 加密后的授权字符串
*/
public static String generateAuth(String versionType, int maxResourceCount, int authDays, String serverHardwareMd5) {
// 创建授权信息对象
AuthInfo authInfo = new AuthInfo();
authInfo.setVersionType(versionType);
authInfo.setMaxResourceCount(maxResourceCount);
authInfo.setAuthDays(authDays);
authInfo.setServerHardwareMd5(serverHardwareMd5);
// 设置生成时间和过期时间
Date now = new Date();
authInfo.setGenerateTime(now);
Calendar calendar = Calendar.getInstance();
calendar.setTime(now);
calendar.add(Calendar.DAY_OF_YEAR, authDays);
authInfo.setExpireTime(calendar.getTime());
// 序列化为JSON并加密
String jsonStr = null;
try {
jsonStr = objectMapper.writeValueAsString(authInfo);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
return encrypt(jsonStr);
}
/**
* AES加密
*/
private static String encrypt(String content) {
try {
SecretKeySpec keySpec = new SecretKeySpec(AES_KEY.getBytes(StandardCharsets.UTF_8), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encrypted = cipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) {
throw new RuntimeException("授权信息加密失败", e);
}
}
}

View File

@ -0,0 +1,32 @@
package com.yj.earth.auth;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
/**
* 授权信息模型类
* 存储授权相关的所有信息
*/
@Data
public class AuthInfo {
// 版本类型
private String versionType;
// 最大可加载资源数量
private int maxResourceCount;
// 授权天数
private int authDays;
// 服务器硬件信息MD5
private String serverHardwareMd5;
// 授权生成时间
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date generateTime;
// 授权过期时间
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date expireTime;
}

View File

@ -0,0 +1,115 @@
package com.yj.earth.auth;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yj.earth.common.util.ServerUniqueIdUtil;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Date;
/**
* 授权校验工具类
*/
public class AuthValidator {
// 与生成工具类使用相同的AES密钥
private static final String AES_KEY = "7AJD6H5AGHY6SJU7";
// Jackson JSON处理器
private static final ObjectMapper objectMapper = new ObjectMapper();
/**
* 校验授权是否有效
*
* @param authString 加密的授权字符串
* @param currentServerHardwareMd5 当前服务器硬件信息MD5
* @return 授权是否有效的布尔值
*/
public static boolean validateAuth(String authString, String currentServerHardwareMd5) {
try {
// 解密授权信息
AuthInfo authInfo = getAuthInfo(authString);
// 校验服务器硬件信息是否匹配
if (!authInfo.getServerHardwareMd5().equals(currentServerHardwareMd5)) {
return false;
}
// 校验授权是否过期
Date now = new Date();
return now.before(authInfo.getExpireTime());
} catch (Exception e) {
// 解密失败或格式错误均视为无效授权
return false;
}
}
/**
* 获取授权详情
*
* @param authString 加密的授权字符串
* @return 授权信息对象
* @throws Exception 解密失败或格式错误时抛出异常
*/
public static AuthInfo getAuthInfo(String authString) {
try {
// 解密
String decrypted = decrypt(authString);
// 转换为 AuthInfo 对象
return objectMapper.readValue(decrypted, AuthInfo.class);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* AES解密
*/
private static String decrypt(String encryptedContent) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(AES_KEY.getBytes(StandardCharsets.UTF_8), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(encryptedContent));
return new String(decrypted, StandardCharsets.UTF_8);
}
/**
* 获取授权剩余天数
*/
public static int getRemainingDays(String authString){
try {
AuthInfo authInfo = getAuthInfo(authString);
Date now = new Date();
// 如果已过期、返回0
if (now.after(authInfo.getExpireTime())) {
return 0;
}
// 计算剩余天数
long remainingMillis = authInfo.getExpireTime().getTime() - now.getTime();
return (int) (remainingMillis / (24 * 60 * 60 * 1000)) + 1;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
String serverUniqueId = ServerUniqueIdUtil.getServerUniqueId();
// 生成授权
String authString = AuthGenerator.generateAuth("标准版", 1000, 30, serverUniqueId);
System.out.println("授权字符串:" + authString);
// 验证授权
boolean isValid = AuthValidator.validateAuth(authString, serverUniqueId);
System.out.println("授权是否有效:" + isValid);
int remainingDays = AuthValidator.getRemainingDays(authString);
System.out.println("剩余天数:" + remainingDays);
AuthInfo authInfo = AuthValidator.getAuthInfo(authString);
System.out.println("授权信息:" + authInfo);
}
}

View File

@ -0,0 +1,86 @@
package com.yj.earth.business.controller;
import com.yj.earth.auth.AuthInfo;
import com.yj.earth.auth.AuthValidator;
import com.yj.earth.common.util.ApiResponse;
import com.yj.earth.common.util.ServerUniqueIdUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@Tag(name = "系统设置管理")
@RestController
@RequestMapping("/auth")
public class AuthController {
// 授权文件存储路径、项目根目录下的license目录
private static final String AUTH_FILE_PATH = "license/yjearth.lic";
@GetMapping("/info")
@Operation(summary = "获取系统授权码")
public ApiResponse info() {
return ApiResponse.success(ServerUniqueIdUtil.getServerUniqueId());
}
@PostMapping("/import")
@Operation(summary = "导入授权信息")
public ApiResponse importAuth(@Parameter(description = "授权文件", required = true) @RequestParam("file") MultipartFile file) {
// 验证文件是否为空
if (file.isEmpty()) {
return ApiResponse.failure("请选择授权文件");
}
// 验证文件名是否为 yjearth.lic
String fileName = file.getOriginalFilename();
if (fileName == null || !fileName.equals("yjearth.lic")) {
return ApiResponse.failure("请上传 yjearth.lic");
}
try {
// 读取文件内容
String authContent = new String(file.getBytes(), StandardCharsets.UTF_8).trim();
// 验证授权内容有效性
String serverHardwareMd5 = ServerUniqueIdUtil.getServerUniqueId();
boolean isValid = AuthValidator.validateAuth(authContent, serverHardwareMd5);
if (!isValid) {
return ApiResponse.failure("授权文件无效或已过期");
}
// 创建目录(如果不存在)
Path path = Paths.get(AUTH_FILE_PATH);
if (!Files.exists(path.getParent())) {
Files.createDirectories(path.getParent());
}
// 保存授权文件
Files.write(path, authContent.getBytes(StandardCharsets.UTF_8));
return ApiResponse.success(null);
} catch (Exception e) {
return ApiResponse.failure("导入授权文件失败:" + e.getMessage());
}
}
@GetMapping("/show")
@Operation(summary = "查看授权信息")
public ApiResponse showAuth() {
try {
// 检查授权文件是否存在
Path path = Paths.get(AUTH_FILE_PATH);
if (!Files.exists(path)) {
return ApiResponse.failure("请先导入授权");
}
// 读取授权文件内容
String authContent = new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
// 获取授权详情
AuthInfo authInfo = AuthValidator.getAuthInfo(authContent);
return ApiResponse.success(authInfo);
} catch (Exception e) {
return ApiResponse.failure("获取授权信息失败:" + e.getMessage());
}
}
}

View File

@ -0,0 +1,241 @@
package com.yj.earth.business.controller;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yj.earth.business.domain.FileInfo;
import com.yj.earth.business.service.FileInfoService;
import com.yj.earth.common.util.ApiResponse;
import com.yj.earth.vo.FileInfoVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Tag(name = "文件数据管理")
@RestController
@RequestMapping("/fileInfo")
public class FileInfoController {
@Resource
private FileInfoService fileInfoService;
@Value("${file.upload.path}")
private String uploadPath;
// 获取项目根目录
private String getProjectRootPath() {
return System.getProperty("user.dir");
}
// 获取完整的上传目录路径
private String getFullUploadPath() {
// 拼接项目根目录和配置的上传路径
return getProjectRootPath() + File.separator + uploadPath;
}
@Operation(summary = "文件上传")
@PostMapping("/upload")
public ApiResponse uploadFiles(@Parameter(description = "上传的文件数组", required = true) @RequestParam("files") MultipartFile[] files) throws IOException {
// 校验文件数组是否为空
if (files == null || files.length == 0) {
return ApiResponse.failure("上传文件不能为空");
}
// 获取完整的上传目录路径
String fullUploadPath = getFullUploadPath();
List<FileInfoVo> fileInfoVoList = new ArrayList<>();
// 遍历处理每个文件
for (MultipartFile file : files) {
// 跳过空文件
if (file.isEmpty()) {
continue;
}
// 获取原始文件名和后缀
String originalFilename = file.getOriginalFilename();
String fileSuffix = FileUtil.extName(originalFilename);
String contentType = file.getContentType();
// 生成唯一文件名、避免重复
String uniqueFileName = IdUtil.simpleUUID() + "." + fileSuffix;
// 创建文件存储目录(如果不存在则自动创建)
File uploadDir = new File(fullUploadPath);
FileUtil.mkdir(uploadDir);
// 构建完整文件路径
String filePath = fullUploadPath + File.separator + uniqueFileName;
File destFile = new File(filePath);
// 使用Hutool工具类保存文件
FileUtil.writeBytes(file.getBytes(), destFile);
// 计算文件MD5用于校验文件完整性
String fileMd5 = DigestUtil.md5Hex(destFile);
// 查询有没有文件名一样并且 MD5 也一样的数据
LambdaQueryWrapper<FileInfo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(FileInfo::getFileName, originalFilename).eq(FileInfo::getFileMd5, fileMd5);
if (fileInfoService.count(queryWrapper) > 0) { // 修复了此处的bug、添加了queryWrapper参数
return ApiResponse.failure("已存在文件名相同且内容完全一致的文件");
}
// 保存文件信息到数据库
FileInfo fileInfo = new FileInfo();
fileInfo.setFileName(originalFilename);
fileInfo.setFileSuffix(fileSuffix);
fileInfo.setContentType(contentType);
fileInfo.setFileSize(file.getSize());
fileInfo.setFilePath(uniqueFileName); // 只保存相对文件名、不保存完整路径
fileInfo.setFileMd5(fileMd5);
// 保存文件信息并获取ID
fileInfoService.save(fileInfo);
// 构建并设置预览URL和下载URL
String previewUrl = "/fileInfo/preview/" + fileInfo.getId();
String downloadUrl = "/fileInfo/download/" + fileInfo.getId();
FileInfoVo fileInfoVo = new FileInfoVo();
BeanUtils.copyProperties(fileInfo, fileInfoVo);
fileInfoVo.setPreviewUrl(previewUrl);
fileInfoVo.setDownloadUrl(downloadUrl);
fileInfoVoList.add(fileInfoVo);
}
if (fileInfoVoList.isEmpty()) {
return ApiResponse.failure("未成功上传任何文件");
}
return ApiResponse.success(fileInfoVoList);
}
@Operation(summary = "文件下载")
@GetMapping("/download/{id}")
public void downloadFile(@Parameter(description = "文件ID", required = true) @PathVariable String id, HttpServletResponse response) throws IOException {
// 根据ID查询文件信息
FileInfo fileInfo = fileInfoService.getById(id);
if (fileInfo == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 构建完整文件路径
String fullPath = getFullUploadPath() + File.separator + fileInfo.getFilePath();
File file = new File(fullPath);
// 校验文件是否存在
if (!file.exists()) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 设置响应头
response.setContentType(fileInfo.getContentType());
response.setContentLengthLong(fileInfo.getFileSize());
String encodedFileName = URLEncoder.encode(fileInfo.getFileName(), StandardCharsets.UTF_8.name());
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + encodedFileName + "\"; filename*=UTF-8''" + encodedFileName);
// 使用 Hutool 工具类复制文件流到响应输出流
try (OutputStream os = response.getOutputStream()) {
FileUtil.writeToStream(file, os);
}
}
@Operation(summary = "文件预览")
@GetMapping("/preview/{id}")
public void previewFile(
@Parameter(description = "文件ID", required = true)
@PathVariable String id,
HttpServletResponse response) throws IOException {
// 根据ID查询文件信息
FileInfo fileInfo = fileInfoService.getById(id);
if (fileInfo == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 构建完整文件路径
String fullPath = getFullUploadPath() + File.separator + fileInfo.getFilePath();
File file = new File(fullPath);
// 校验文件是否存在
if (!file.exists()) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 设置响应头(不设置 attachment、让浏览器直接显示
response.setContentType(fileInfo.getContentType());
response.setContentLengthLong(fileInfo.getFileSize());
// 使用 Hutool 工具类复制文件流到响应输出流
try (OutputStream os = response.getOutputStream()) {
FileUtil.writeToStream(file, os);
}
}
@Operation(summary = "文件列表")
@GetMapping("/list")
public ApiResponse getFileList(
@Parameter(description = "页码", required = true) Integer pageNum,
@Parameter(description = "每页条数", required = true) Integer pageSize,
@Parameter(description = "文件名称") String fileName) {
// 创建分页对象
Page<FileInfo> page = new Page<>(pageNum, pageSize);
// 构建查询条件
LambdaQueryWrapper<FileInfo> queryWrapper = new LambdaQueryWrapper<>();
if (fileName != null && !fileName.isEmpty()) {
queryWrapper.like(FileInfo::getFileName, fileName);
}
// 按创建时间倒序排列、最新上传的文件在前
queryWrapper.orderByDesc(FileInfo::getCreatedAt);
// 执行分页查询
IPage<FileInfo> fileInfoPage = fileInfoService.page(page, queryWrapper);
// 转换为VO对象并设置URL
List<FileInfoVo> records = fileInfoPage.getRecords().stream().map(fileInfo -> {
FileInfoVo vo = new FileInfoVo();
BeanUtils.copyProperties(fileInfo, vo);
vo.setPreviewUrl("/fileInfo/preview/" + fileInfo.getId());
vo.setDownloadUrl("/fileInfo/download/" + fileInfo.getId());
return vo;
}).collect(Collectors.toList());
// 构建分页结果
Page<FileInfoVo> resultPage = new Page<>();
resultPage.setRecords(records);
resultPage.setTotal(fileInfoPage.getTotal());
resultPage.setSize(fileInfoPage.getSize());
resultPage.setCurrent(fileInfoPage.getCurrent());
resultPage.setPages(fileInfoPage.getPages());
return ApiResponse.success(resultPage);
}
}

View File

@ -0,0 +1,237 @@
package com.yj.earth.business.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.graphhopper.GHRequest;
import com.graphhopper.GHResponse;
import com.graphhopper.GraphHopper;
import com.graphhopper.ResponsePath;
import com.graphhopper.config.Profile;
import com.graphhopper.util.shapes.GHPoint;
import com.yj.earth.business.domain.FileInfo;
import com.yj.earth.business.service.FileInfoService;
import com.yj.earth.common.config.GraphHopperProperties;
import com.yj.earth.common.util.ApiResponse;
import com.yj.earth.model.Point;
import com.yj.earth.model.RouteRequest;
import com.yj.earth.model.RouteResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
@Data
@Slf4j
@Tag(name = "路径规划管理")
@RestController
@RequestMapping("/graphhopper")
public class GraphHopperController {
@Resource
private FileInfoService fileInfoService;
@Resource
private GraphHopperProperties graphHopperProperties;
// 存储当前可用的 GraphHopper 实例
private volatile GraphHopper currentHopper;
// 状态控制: 线程安全
private final AtomicBoolean isLoading = new AtomicBoolean(false);
private final AtomicBoolean isLoaded = new AtomicBoolean(false);
@Operation(summary = "获取地图列表")
@GetMapping("/list")
public ApiResponse list() {
LambdaQueryWrapper<FileInfo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(FileInfo::getFileSuffix, "pbf");
return ApiResponse.success(fileInfoService.list(queryWrapper));
}
@Operation(summary = "加载地图数据")
@PostMapping("/loadMap")
public ApiResponse loadMap(@Parameter(description = "文件ID") @RequestParam String fileId) {
// 参数校验
if (fileId == null) {
return ApiResponse.failure("文件ID不能为空");
}
// 获取并校验OSM文件
String osmFilePath = fileInfoService.getFileAbsolutePath(fileId);
File osmFile = new File(osmFilePath);
if (!osmFile.exists()) {
return ApiResponse.failure("地图文件不存在: " + osmFilePath);
}
if (!osmFile.isFile() || !osmFile.getName().endsWith(".pbf")) {
return ApiResponse.failure("仅支持有效的.pbf格式OSM文件");
}
// 防止并发加载
if (isLoading.get()) {
return ApiResponse.failure("地图正在加载中、请稍后查询状态");
}
// 标记加载状态
isLoading.set(true);
isLoaded.set(false);
// 异步执行: 删除旧数据 → 创建新实例 → 加载新地图
new Thread(() -> {
GraphHopper newHopper = null;
try {
// 关键步骤1: 彻底删除旧地图数据目录
deleteOldGraphDir();
// 关键步骤2: 创建全新的GraphHopper实例
newHopper = createNewGraphHopperInstance(osmFilePath);
// 关键步骤3: 加载新地图
newHopper.importOrLoad();
// 关键步骤4: 加载成功 → 替换当前实例 + 更新状态
currentHopper = newHopper;
isLoaded.set(true);
log.info("地图加载成功");
} catch (Exception e) {
// 加载失败 → 清理新实例资源
if (newHopper != null) {
newHopper.close();
}
isLoaded.set(false);
e.printStackTrace();
log.error("地图加载失败: " + e.getMessage());
} finally {
// 无论成功/失败、释放加载锁
isLoading.set(false);
}
}).start();
return ApiResponse.success(null);
}
@Operation(summary = "路径规划")
@PostMapping("/route")
public ApiResponse calculateRoute(@RequestBody RouteRequest request) {
// 校验地图是否加载完成 + 实例是否可用
if (!isLoaded.get() || currentHopper == null) {
return ApiResponse.failure("地图未加载完成");
}
try {
// 构建路径点列表
List<GHPoint> ghPoints = new ArrayList<>();
ghPoints.add(new GHPoint(request.getStartLat(), request.getStartLng())); // 起点
// 添加途经点
if (request.getWaypoints() != null && !request.getWaypoints().isEmpty()) {
for (Point waypoint : request.getWaypoints()) {
ghPoints.add(new GHPoint(waypoint.getLat(), waypoint.getLng()));
}
}
ghPoints.add(new GHPoint(request.getEndLat(), request.getEndLng())); // 终点
// 构建请求仅指定Profile、无setWeighting
String targetProfile = request.getProfile() != null ? request.getProfile() : "car";
GHRequest ghRequest = new GHRequest(ghPoints)
.setProfile(targetProfile);
// 用新实例计算路径
GHResponse response = currentHopper.route(ghRequest);
// 处理错误
if (response.hasErrors()) {
return ApiResponse.failure("路径计算失败: " + response.getErrors().toString());
}
// 解析结果
ResponsePath bestPath = response.getBest();
List<Point> pathPoints = new ArrayList<>();
bestPath.getPoints().forEach(ghPoint ->
pathPoints.add(new Point(ghPoint.getLat(), ghPoint.getLon()))
);
// 封装返回
RouteResponse routeResponse = new RouteResponse(bestPath.getDistance() / 1000, (double) (bestPath.getTime() / 60000), pathPoints);
return ApiResponse.success(routeResponse);
} catch (Exception e) {
return ApiResponse.failure("路径计算异常: " + e.getMessage());
}
}
@Operation(summary = "获取交通方式")
@PostMapping("/profiles")
public ApiResponse profiles() {
return ApiResponse.success(graphHopperProperties.getProfiles());
}
/**
* 创建全新的 GraphHopper 实例
*/
private GraphHopper createNewGraphHopperInstance(String osmFilePath) {
GraphHopper hopper = new GraphHopper();
// 配置基础参数
hopper.setOSMFile(osmFilePath);
hopper.setGraphHopperLocation(graphHopperProperties.getGraphLocation());
// 配置交通方式 + 权重策略
List<Profile> profileList = new ArrayList<>();
for (String profileName : graphHopperProperties.getProfiles()) {
Profile profile = new Profile(profileName);
profile.setVehicle(profileName);
profile.setWeighting("fastest");
profileList.add(profile);
}
hopper.setProfiles(profileList);
return hopper;
}
/**
* 递归删除旧地图数据目录
*/
private void deleteOldGraphDir() {
File graphDir = new File(graphHopperProperties.getGraphLocation());
if (!graphDir.exists()) {
log.info("旧地图目录不存在、无需删除: " + graphHopperProperties.getGraphLocation());
return;
}
// 递归删除所有文件和子目录
File[] files = graphDir.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
deleteOldGraphDir(file);
} else {
boolean deleted = file.delete();
log.info("删除旧地图文件: " + file.getAbsolutePath() + "" + (deleted ? "成功" : "失败"));
}
}
}
// 删除空目录
boolean dirDeleted = graphDir.delete();
System.out.println("删除旧地图目录: " + graphDir.getAbsolutePath() + "" + (dirDeleted ? "成功" : "失败"));
}
// 重载: 递归删除子目录
private void deleteOldGraphDir(File subDir) {
File[] files = subDir.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
deleteOldGraphDir(file);
} else {
file.delete();
}
}
}
subDir.delete();
}
}

View File

@ -0,0 +1,62 @@
package com.yj.earth.business.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yj.earth.business.domain.Role;
import com.yj.earth.business.service.RoleService;
import com.yj.earth.common.util.ApiResponse;
import com.yj.earth.dto.role.AddRoleDto;
import com.yj.earth.dto.role.UpdateRoleDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@Tag(name = "角色数据管理")
@RestController
@RequestMapping("/role")
public class RoleController {
@Resource
private RoleService roleService;
@Operation(summary = "新增角色")
@PostMapping("/add")
public ApiResponse save(@RequestBody AddRoleDto addRoleDto) {
Role role = new Role();
BeanUtils.copyProperties(addRoleDto, role);
roleService.save(role);
return ApiResponse.success(null);
}
@Operation(summary = "删除角色")
@PostMapping("/delete")
public ApiResponse delete(@Parameter(description = "角色ID") String id) {
roleService.removeById(id);
return ApiResponse.success(null);
}
@Operation(summary = "更新角色")
@PostMapping("/update")
public ApiResponse update(@RequestBody UpdateRoleDto updateRoleDto) {
Role role = new Role();
BeanUtils.copyProperties(updateRoleDto, role);
roleService.updateById(role);
return ApiResponse.success(null);
}
@Operation(summary = "角色详情")
@GetMapping("/getById")
public ApiResponse get(@Parameter(description = "角色ID") String id) {
Role role = roleService.getById(id);
return ApiResponse.success(role);
}
@Operation(summary = "角色列表")
@GetMapping("/list")
public ApiResponse list(@Parameter(description = "分页数量") Integer pageNum, @Parameter(description = "分页大小") Integer pageSize) {
Page<Role> rolePage = roleService.page(new Page<>(pageNum, pageSize));
return ApiResponse.success(rolePage);
}
}

View File

@ -0,0 +1,44 @@
package com.yj.earth.business.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.yj.earth.business.domain.RoleSource;
import com.yj.earth.business.service.RoleService;
import com.yj.earth.business.service.RoleSourceService;
import com.yj.earth.business.service.SourceService;
import com.yj.earth.common.util.ApiResponse;
import com.yj.earth.dto.relation.RoleBindOrUnBindSourceDto;
import com.yj.earth.dto.relation.SourceBindOrUnBindRoleDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;
@Tag(name = "角色资源管理")
@RestController
@RequestMapping("/roleSource")
public class RoleSourceController {
@Resource
private RoleSourceService roleSourceService;
@Operation(summary = "角色绑定资源")
@PostMapping("/roleBindSource")
public ApiResponse roleBindSource(@RequestBody RoleBindOrUnBindSourceDto roleBindOrUnBindSourceDto) {
// 先删除该角色下的所有资源
roleSourceService.remove(new LambdaQueryWrapper<RoleSource>().eq(RoleSource::getRoleId, roleBindOrUnBindSourceDto.getRoleId()));
// 再设置新的资源
for (String sourceId : roleBindOrUnBindSourceDto.getSourceIdList()) {
RoleSource roleSource = new RoleSource();
roleSource.setRoleId(roleBindOrUnBindSourceDto.getRoleId());
roleSource.setSourceId(sourceId);
roleSourceService.save(roleSource);
}
return ApiResponse.success(null);
}
}

View File

@ -0,0 +1,168 @@
package com.yj.earth.business.controller;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.io.FileUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yj.earth.business.domain.Source;
import com.yj.earth.business.service.RoleSourceService;
import com.yj.earth.business.service.SourceService;
import com.yj.earth.business.service.UserService;
import com.yj.earth.common.service.SourceParamsValidator;
import com.yj.earth.common.util.ApiResponse;
import com.yj.earth.common.util.MapUtil;
import com.yj.earth.dto.source.AddDirectoryDto;
import com.yj.earth.dto.source.AddModelSourceDto;
import com.yj.earth.dto.source.AddOtherSourceDto;
import com.yj.earth.dto.source.UpdateSourceDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Map;
import java.util.Set;
import static com.yj.earth.common.constant.GlobalConstant.DIRECTORY;
import static com.yj.earth.common.constant.GlobalConstant.SHOW;
@Slf4j
@Tag(name = "树形结构管理")
@RestController
@RequestMapping("/source")
public class SourceController {
@Resource
private SourceService sourceService;
@Resource
private UserService userService;
@Resource
private RoleSourceService roleSourceService;
@Resource
private SourceParamsValidator sourceParamsValidator;
@PostMapping("/addDirectory")
@Operation(summary = "新增目录资源")
public ApiResponse addDirectory(@RequestBody AddDirectoryDto addDirectoryDto) {
// 校验是否通过
String message = sourceService.checkIsPass(addDirectoryDto.getParentId(), addDirectoryDto.getSourceName());
if (message != null) {
return ApiResponse.failure(message);
}
// 通过之后保存资源
Source source = new Source();
BeanUtils.copyProperties(addDirectoryDto, source);
source.setSourceType(DIRECTORY);
source.setIsShow(SHOW);
sourceService.save(source);
// 添加资源到该用户的角色下
roleSourceService.addRoleSource(userService.getById(StpUtil.getLoginIdAsString()).getRoleId(), source.getId());
return ApiResponse.success(source);
}
@Operation(summary = "新增模型资源")
@PostMapping("/addModelSource")
public ApiResponse addModelSource(@RequestBody AddModelSourceDto addModelSourceDto) {
// 获取资源绝对路径
String sourcePath = addModelSourceDto.getSourcePath();
// 获取资源名称
String sourceName = FileUtil.mainName(sourcePath);
// 校验是否通过
String message = sourceService.checkIsPass(addModelSourceDto.getParentId(), sourceName);
if (message != null) {
return ApiResponse.failure(message);
}
// 调用SDK加载资源
String sourceId = sourceService.addAndGetSourceId(sourcePath);
// 获取文件路径并处理详情
String detail = sourceService.getDetail(sourcePath, sourceId);
// 构建并保存资源对象
Source source = new Source();
source.setSourcePath(sourcePath);
source.setSourceName(sourceName);
source.setParentId(addModelSourceDto.getParentId());
source.setTreeIndex(addModelSourceDto.getTreeIndex());
source.setDetail(detail);
source.setSourceType(MapUtil.getString(MapUtil.jsonToMap(detail), "fileType"));
source.setIsShow(SHOW);
source.setParams(addModelSourceDto.getParams());
sourceService.save(source);
// 添加资源到该用户的角色下
roleSourceService.addRoleSource(userService.getById(StpUtil.getLoginIdAsString()).getRoleId(), source.getId());
return ApiResponse.success(source);
}
@Operation(summary = "新增其他资源")
@PostMapping("/addOtherSource")
public ApiResponse addOtherSource(@RequestBody AddOtherSourceDto addOtherSourceDto) throws JsonProcessingException {
// 校验是否通过
String message = sourceService.checkIsPass(addOtherSourceDto.getParentId(), addOtherSourceDto.getSourceName());
if (message != null) {
return ApiResponse.failure(message);
}
// 验证并转换参数
Object validatedParams = sourceParamsValidator.validateAndConvert(
addOtherSourceDto.getSourceType(),
addOtherSourceDto.getParams()
);
System.out.println(validatedParams);
Source source = new Source();
BeanUtils.copyProperties(addOtherSourceDto, source);
source.setIsShow(SHOW);
source.setParams(MapUtil.objectToJson(validatedParams));
sourceService.save(source);
// 添加资源到该用户的角色下
roleSourceService.addRoleSource(userService.getById(StpUtil.getLoginIdAsString()).getRoleId(), source.getId());
return ApiResponse.success(source);
}
@Operation(summary = "更新资源信息及参数")
@PostMapping("/update")
public ApiResponse updateSource(@RequestBody UpdateSourceDto updateSourceDto) {
// 查询资源
Source source = sourceService.getById(updateSourceDto.getId());
if (source == null) {
return ApiResponse.failure("资源不存在");
}
// 更新基本信息
BeanUtils.copyProperties(updateSourceDto, source);
// 处理参数更新
if (updateSourceDto.getParams() != null && !updateSourceDto.getParams().isEmpty()) {
// 获取类型
String sourceType = source.getSourceType();
// 验证参数
Object validatedParams = sourceParamsValidator.validateAndConvert(
sourceType,
updateSourceDto.getParams()
);
// 获取原始数据的 Map 并合并新参数
Map<String, Object> dataMap = MapUtil.jsonToMap(source.getParams());
MapUtil.mergeMaps(dataMap, updateSourceDto.getParams());
source.setParams(MapUtil.mapToString(dataMap));
}
// 保存更新
sourceService.updateById(source);
return ApiResponse.success(source);
}
@GetMapping("/type")
@Operation(summary = "获取支持的资源类型")
public ApiResponse getSupportedSourceTypes() {
Set<String> supportedTypes = sourceParamsValidator.getSupportedSourceTypes();
return ApiResponse.success(supportedTypes);
}
@Operation(summary = "获取资源列表")
@GetMapping("/list")
public ApiResponse list() {
return ApiResponse.success(sourceService.getSourceListByUserId(StpUtil.getLoginIdAsString()));
}
}

View File

@ -0,0 +1,140 @@
package com.yj.earth.business.controller;
import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.crypto.digest.BCrypt;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yj.earth.annotation.EncryptResponse;
import com.yj.earth.annotation.ExcludeField;
import com.yj.earth.annotation.RoleAccess;
import com.yj.earth.business.domain.Role;
import com.yj.earth.business.domain.User;
import com.yj.earth.business.service.RoleService;
import com.yj.earth.dto.relation.UserBindOrUnBindRoleDto;
import com.yj.earth.dto.user.AddUserDto;
import com.yj.earth.dto.user.UpdatePasswordDto;
import com.yj.earth.dto.user.UpdateUserDto;
import com.yj.earth.dto.user.UserLoginDto;
import com.yj.earth.business.service.UserService;
import com.yj.earth.common.util.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
@Tag(name = "用户数据管理")
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
@Resource
private RoleService roleService;
@Operation(summary = "新增用户")
@PostMapping("/add")
@RoleAccess(roleNames = "管理员")
public ApiResponse save(@RequestBody AddUserDto addUserDto) {
User user = new User();
BeanUtils.copyProperties(addUserDto, user);
if (userService.getOne(new LambdaQueryWrapper<User>().eq(User::getUsername, user.getUsername())) != null) {
return ApiResponse.failure("用户已存在");
}
String password = user.getPassword();
user.setPassword(BCrypt.hashpw(password, BCrypt.gensalt()));
if (addUserDto.getRoleId() == null) {
// 查询系统名字为默认角色的角色ID
user.setRoleId(roleService.getOne(new LambdaQueryWrapper<Role>().eq(Role::getRoleName, "默认角色")).getId());
}
userService.save(user);
return ApiResponse.success(null);
}
@Operation(summary = "删除用户")
@PostMapping("/delete")
@RoleAccess(roleNames = "管理员")
public ApiResponse delete(@Parameter(description = "用户ID") String id) {
userService.removeById(id);
return ApiResponse.success(null);
}
@Operation(summary = "更新信息")
@PostMapping("/update")
public ApiResponse update(@RequestBody UpdateUserDto updateUserDto) {
User user = new User();
BeanUtils.copyProperties(updateUserDto, user);
userService.updateById(user);
return ApiResponse.success(null);
}
@Operation(summary = "更新密码")
@PostMapping("/updatePassword")
public ApiResponse updatePassword(@RequestBody UpdatePasswordDto updatePasswordDto) {
User user = userService.getById(updatePasswordDto.getId());
if (user == null) {
return ApiResponse.failure("用户不存在");
}
if (!BCrypt.checkpw(updatePasswordDto.getOldPassword(), user.getPassword())) {
return ApiResponse.failure("旧密码错误");
}
user.setPassword(BCrypt.hashpw(updatePasswordDto.getNewPassword(), BCrypt.gensalt()));
userService.updateById(user);
return ApiResponse.success(null);
}
@Operation(summary = "用户详情")
@GetMapping("/getById")
public ApiResponse get(@Parameter(description = "用户ID") String id) {
return ApiResponse.success(userService.getById(id));
}
@Operation(summary = "用户列表")
@GetMapping("/list")
@RoleAccess(roleNames = "管理员")
public ApiResponse list(@Parameter(description = "分页数量") Integer pageNum, @Parameter(description = "分页大小") Integer pageSize) {
Page<User> userPage = userService.page(new Page<>(pageNum, pageSize));
return ApiResponse.success(userPage);
}
@Operation(summary = "用户登录")
@PostMapping("/login")
public ApiResponse login(@RequestBody UserLoginDto userLoginDto) {
User user = userService.getOne(new LambdaQueryWrapper<User>().eq(User::getUsername, userLoginDto.getUsername()));
if (user == null) {
return ApiResponse.failure("用户名不存在");
}
if (!BCrypt.checkpw(userLoginDto.getPassword(), user.getPassword())) {
return ApiResponse.failure("密码错误");
}
StpUtil.login(user.getId());
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
return ApiResponse.success(Map.of("header", tokenInfo.getTokenName(), "token", tokenInfo.getTokenValue()));
}
@Operation(summary = "用户登出")
@PostMapping("/logout")
public ApiResponse logout() {
StpUtil.logout();
return ApiResponse.success(null);
}
@Operation(summary = "设置角色")
@PostMapping("/userBindOrUnBindRole")
@RoleAccess(roleNames = "管理员")
public ApiResponse userBindOrUnBindRole(@RequestBody UserBindOrUnBindRoleDto userBindOrUnBindRoleDto) {
userService.lambdaUpdate().set(User::getRoleId, userBindOrUnBindRoleDto.getRoleId()).eq(User::getId, userBindOrUnBindRoleDto.getUserId()).update();
return ApiResponse.success(null);
}
@Operation(summary = "获取当前用户信息")
@GetMapping("/getCurrentUserInfo")
public ApiResponse getCurrentUserInfo() {
return ApiResponse.success(userService.getById(StpUtil.getLoginIdAsString()));
}
}

View File

@ -0,0 +1,50 @@
package com.yj.earth.business.domain;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.time.LocalDateTime;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
@Data
public class FileInfo implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "主键")
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
@Schema(description = "文件名")
private String fileName;
@Schema(description = "文件后缀")
private String fileSuffix;
@Schema(description = "内容类型")
private String contentType;
@Schema(description = "文件大小")
private Long fileSize;
@Schema(description = "文件路径")
private String filePath;
@Schema(description = "文件MD5")
private String fileMd5;
@Schema(description = "创建时间")
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.UPDATE)
private LocalDateTime updatedAt;
}

View File

@ -0,0 +1,41 @@
package com.yj.earth.business.domain;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import java.time.LocalDateTime;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
@Data
public class Role implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "主键")
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
@Schema(description = "角色名称")
private String roleName;
@Schema(description = "角色描述")
private String description;
@Schema(description = "是否超级管理员")
private Integer isSuper;
@Schema(description = "创建时间")
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@Schema(description = "更新时间")
@TableField(fill = FieldFill.UPDATE)
private LocalDateTime updatedAt;
}

View File

@ -0,0 +1,39 @@
package com.yj.earth.business.domain;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.time.LocalDateTime;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
@Data
public class RoleSource implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "主键")
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
@Schema(description = "角色ID")
private String roleId;
@Schema(description = "资源ID")
private String sourceId;
@Schema(description = "创建时间")
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@Schema(description = "更新时间")
@TableField(fill = FieldFill.UPDATE)
private LocalDateTime updatedAt;
}

View File

@ -0,0 +1,56 @@
package com.yj.earth.business.domain;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import java.time.LocalDateTime;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
@Data
public class Source implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "主键")
@TableId(value = "id", type = IdType.INPUT)
private String id;
@Schema(description = "资源名称")
private String sourceName;
@Schema(description = "资源类型")
private String sourceType;
@Schema(description = "资源路径")
private String sourcePath;
@Schema(description = "父级ID")
private String parentId;
@Schema(description = "树形索引")
private Integer treeIndex;
@Schema(description = "是否显示")
private Integer isShow;
@Schema (description = "其他内容")
private String detail;
@Schema (description = "前端参数")
private String params;
@Schema(description = "创建时间")
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@Schema(description = "更新时间")
@TableField(fill = FieldFill.UPDATE)
private LocalDateTime updatedAt;
}

View File

@ -0,0 +1,51 @@
package com.yj.earth.business.domain;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import java.time.LocalDateTime;
import com.yj.earth.annotation.ExcludeField;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
@Data
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
@Schema(description = "用户名")
private String username;
@Schema(description = "密码")
@ExcludeField
private String password;
@Schema(description = "头像")
private String avatar;
@Schema(description = "昵称")
private String nickname;
@Schema(description = "手机号")
private String phone;
@Schema(description = "所属角色")
private String roleId;
@Schema(description = "创建时间")
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@Schema(description = "更新时间")
@TableField(fill = FieldFill.UPDATE)
private LocalDateTime updatedAt;
}

View File

@ -0,0 +1,18 @@
package com.yj.earth.business.mapper;
import com.yj.earth.business.domain.FileInfo;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* <p>
* Mapper 接口
* </p>
*
* @author 周志雄
* @since 2025-08-29
*/
@Mapper
public interface FileInfoMapper extends BaseMapper<FileInfo> {
}

View File

@ -0,0 +1,18 @@
package com.yj.earth.business.mapper;
import com.yj.earth.business.domain.Role;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* <p>
* Mapper 接口
* </p>
*
* @author 周志雄
* @since 2025-08-28
*/
@Mapper
public interface RoleMapper extends BaseMapper<Role> {
}

View File

@ -0,0 +1,18 @@
package com.yj.earth.business.mapper;
import com.yj.earth.business.domain.RoleSource;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* <p>
* Mapper 接口
* </p>
*
* @author 周志雄
* @since 2025-08-27
*/
@Mapper
public interface RoleSourceMapper extends BaseMapper<RoleSource> {
}

View File

@ -0,0 +1,18 @@
package com.yj.earth.business.mapper;
import com.yj.earth.business.domain.Source;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* <p>
* Mapper 接口
* </p>
*
* @author 周志雄
* @since 2025-08-26
*/
@Mapper
public interface SourceMapper extends BaseMapper<Source> {
}

View File

@ -0,0 +1,18 @@
package com.yj.earth.business.mapper;
import com.yj.earth.business.domain.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* <p>
* Mapper 接口
* </p>
*
* @author 周志雄
* @since 2025-08-28
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

View File

@ -0,0 +1,9 @@
package com.yj.earth.business.service;
import com.yj.earth.business.domain.FileInfo;
import com.baomidou.mybatisplus.extension.service.IService;
public interface FileInfoService extends IService<FileInfo> {
// 根据文件ID获取文件绝对路径
String getFileAbsolutePath(String id);
}

View File

@ -0,0 +1,16 @@
package com.yj.earth.business.service;
import com.yj.earth.business.domain.Role;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 服务类
* </p>
*
* @author 周志雄
* @since 2025-08-28
*/
public interface RoleService extends IService<Role> {
}

View File

@ -0,0 +1,9 @@
package com.yj.earth.business.service;
import com.yj.earth.business.domain.RoleSource;
import com.baomidou.mybatisplus.extension.service.IService;
public interface RoleSourceService extends IService<RoleSource> {
void addRoleSource(String roleId, String sourceId);
}

View File

@ -0,0 +1,25 @@
package com.yj.earth.business.service;
import com.yj.earth.business.domain.Source;
import com.baomidou.mybatisplus.extension.service.IService;
import com.yj.earth.common.util.ApiResponse;
import java.util.List;
public interface SourceService extends IService<Source> {
String addAndGetSourceId(String sourcePath);
String getDetail(String sourcePath, String sourceId);
String buildSdkUrl(String path);
String fetchCltDetail(String sourceId);
String fetchMbtilesDetail(String sourceId);
String fetchPakDetail(String sourceId);
List<Source> getSourceListByUserId(String userId);
String checkIsPass(String parentId, String sourceName);
}

View File

@ -0,0 +1,16 @@
package com.yj.earth.business.service;
import com.yj.earth.business.domain.User;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 服务类
* </p>
*
* @author 周志雄
* @since 2025-08-28
*/
public interface UserService extends IService<User> {
}

View File

@ -0,0 +1,40 @@
package com.yj.earth.business.service.impl;
import com.yj.earth.business.domain.FileInfo;
import com.yj.earth.business.mapper.FileInfoMapper;
import com.yj.earth.business.service.FileInfoService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import io.swagger.v3.oas.annotations.Parameter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;
import java.io.File;
@Service
public class FileInfoServiceImpl extends ServiceImpl<FileInfoMapper, FileInfo> implements FileInfoService {
@Value("${file.upload.path}")
private String uploadPath;
public String getFileAbsolutePath(String id) {
// 根据ID查询文件信息
FileInfo fileInfo = this.getById(id);
if (fileInfo == null) {
return null;
}
// 构建完整文件路径
String fullPath = uploadPath + File.separator + fileInfo.getFilePath();
File file = new File(fullPath);
// 校验文件是否存在
if (!file.exists()) {
return null;
}
// 获取并返回绝对路径
return file.getAbsolutePath();
}
}

View File

@ -0,0 +1,20 @@
package com.yj.earth.business.service.impl;
import com.yj.earth.business.domain.Role;
import com.yj.earth.business.mapper.RoleMapper;
import com.yj.earth.business.service.RoleService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* <p>
* 服务实现类
* </p>
*
* @author 周志雄
* @since 2025-08-28
*/
@Service
public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService {
}

View File

@ -0,0 +1,20 @@
package com.yj.earth.business.service.impl;
import com.yj.earth.business.domain.RoleSource;
import com.yj.earth.business.mapper.RoleSourceMapper;
import com.yj.earth.business.service.RoleSourceService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
@Service
public class RoleSourceServiceImpl extends ServiceImpl<RoleSourceMapper, RoleSource> implements RoleSourceService {
@Override
public void addRoleSource(String roleId, String sourceId) {
RoleSource roleSource = new RoleSource();
roleSource.setRoleId(roleId);
roleSource.setSourceId(sourceId);
save(roleSource);
}
}

View File

@ -0,0 +1,166 @@
package com.yj.earth.business.service.impl;
import cn.hutool.core.io.FileUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yj.earth.business.domain.Role;
import com.yj.earth.business.domain.RoleSource;
import com.yj.earth.business.domain.Source;
import com.yj.earth.business.domain.User;
import com.yj.earth.business.mapper.SourceMapper;
import com.yj.earth.business.service.RoleService;
import com.yj.earth.business.service.RoleSourceService;
import com.yj.earth.business.service.SourceService;
import com.yj.earth.business.service.UserService;
import com.yj.earth.common.config.ServerConfig;
import com.yj.earth.common.util.ApiResponse;
import com.yj.earth.common.util.HttpUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
@Slf4j
@Service
public class SourceServiceImpl extends ServiceImpl<SourceMapper, Source> implements SourceService {
@Resource
private ServerConfig serverConfig;
@Resource
private RoleSourceService roleSourceService;
@Resource
private UserService userService;
@Resource
private SourceService sourceService;
@Resource
private RoleService roleService;
// 存储文件后缀与对应处理函数的映射关系
public final Map<String, Function<String, String>> detailFetchers;
// 初始化映射关系
public SourceServiceImpl() {
detailFetchers = new HashMap<>();
detailFetchers.put("clt", this::fetchCltDetail);
detailFetchers.put("mbtiles", this::fetchMbtilesDetail);
detailFetchers.put("pak", this::fetchPakDetail);
}
/**
* 调用SDK获取资源ID
*/
@Override
public String addAndGetSourceId(String sourcePath) {
Map<String, Object> addParams = new HashMap<>();
addParams.put("filePath", sourcePath);
String url = buildSdkUrl("/sourceMap/add");
return HttpUtil.doPostForm(url, addParams);
}
/**
* 检测资源是否通过审核
*/
@Override
public String checkIsPass(String parentId, String sourceName) {
// 先查询父节点是否存在
if (parentId != null) {
LambdaQueryWrapper<Source> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Source::getId, parentId);
List<Source> list = sourceService.list(queryWrapper);
if (sourceService.count(queryWrapper) == 0) {
return "父级不存在";
}
}
// // 验证该目录下是否已经存在此资源名一样的
// LambdaQueryWrapper<Source> queryWrapper = new LambdaQueryWrapper<>();
// queryWrapper.eq(Source::getSourceName, sourceName);
// if (sourceService.count(queryWrapper) > 0) {
// return "此目录下已存在此资源";
// }
return null;
}
/**
* 根据文件后缀获取详情信息
*/
@Override
public String getDetail(String sourcePath, String sourceId) {
String ext = FileUtil.extName(sourcePath);
// 通过映射关系获取并执行对应的处理函数
Function<String, String> fetcher = detailFetchers.get(ext);
if (fetcher != null) {
String detailResult = fetcher.apply(sourceId);
return detailResult;
} else {
log.info("未找到{}类型的处理方式", ext);
}
return null;
}
/**
* 构建SDK请求URL
*/
@Override
public String buildSdkUrl(String path) {
return "http://" + serverConfig.getHost() + ":" + serverConfig.getSdkPort() + path;
}
/**
* 获取 CLT 类型资源详情
*/
@Override
public String fetchCltDetail(String sourceId) {
String url = buildSdkUrl("/data/clt/detail/" + sourceId);
return HttpUtil.doGet(url);
}
/**
* 获取 MBTiles 类型资源详情
*/
@Override
public String fetchMbtilesDetail(String sourceId) {
String url = buildSdkUrl("/data/mbtiles/detail/" + sourceId);
return HttpUtil.doGet(url);
}
/**
* 获取 PAK 类型资源详情
*/
@Override
public String fetchPakDetail(String sourceId) {
String url = buildSdkUrl("/data/pak/detail/" + sourceId);
return HttpUtil.doGet(url);
}
/**
* 获取用户资源列表
*/
@Override
public List<Source> getSourceListByUserId(String userId) {
// 查询该用户信息
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getId, userId);
User user = userService.getOne(queryWrapper);
// 查询角色信息
String roleId = user.getRoleId();
LambdaQueryWrapper<Role> roleQueryWrapper = new LambdaQueryWrapper<>();
roleQueryWrapper.eq(Role::getId, roleId);
// 如果这个角色是管理员则直接返回所有资源
if (roleService.getOne(roleQueryWrapper).getIsSuper() == 1) {
return sourceService.list();
}
// 查询属于该角色的资源列表
LambdaQueryWrapper<RoleSource> roleSourceQueryWrapper = new LambdaQueryWrapper<>();
roleSourceQueryWrapper.eq(RoleSource::getRoleId, roleId);
List<RoleSource> roleSourceList = roleSourceService.list(roleSourceQueryWrapper);
// 从结果提取出资源ID列表
List<String> sourceIdList = roleSourceList.stream().map(RoleSource::getSourceId).toList();
return sourceService.listByIds(sourceIdList);
}
}

View File

@ -0,0 +1,20 @@
package com.yj.earth.business.service.impl;
import com.yj.earth.business.domain.User;
import com.yj.earth.business.mapper.UserMapper;
import com.yj.earth.business.service.UserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* <p>
* 服务实现类
* </p>
*
* @author 周志雄
* @since 2025-08-28
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}

View File

@ -0,0 +1,26 @@
package com.yj.earth.common.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import java.util.Collections;
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setAllowedOriginPatterns(Collections.singletonList("*"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}

View File

@ -0,0 +1,25 @@
package com.yj.earth.common.config;
import com.yj.earth.common.exception.UnAuthException;
import com.yj.earth.common.util.ApiResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ApiResponse handleException(Exception e) {
if (!e.getMessage().contains("No static resource")) {
log.error("全局异常处理:{}", e.getMessage());
}
return ApiResponse.failure(e.getMessage());
}
@ExceptionHandler(UnAuthException.class)
public ApiResponse handleUnAuthException(UnAuthException e) {
return ApiResponse.failureWithNoAuth(e.getMessage());
}
}

View File

@ -0,0 +1,15 @@
package com.yj.earth.common.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
@Data
@Component
@ConfigurationProperties(prefix = "graphhopper")
public class GraphHopperProperties {
private String graphLocation;
private List<String> profiles;
}

View File

@ -0,0 +1,42 @@
package com.yj.earth.common.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.yj.earth.annotation.ExcludeField;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* Jackson配置类
*/
@Configuration
public class JacksonConfig {
// 定义日期时间格式
private static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
// 注册 JavaTimeModule 以支持 LocalDateTime 等日期类型
JavaTimeModule javaTimeModule = new JavaTimeModule();
// 配置 LocalDateTime 的序列化格式
LocalDateTimeSerializer localDateTimeSerializer = new LocalDateTimeSerializer(
DateTimeFormatter.ofPattern(DATE_TIME_FORMAT)
);
javaTimeModule.addSerializer(LocalDateTime.class, localDateTimeSerializer);
objectMapper.registerModule(javaTimeModule);
// 配置自定义字段过滤器
SimpleFilterProvider filterProvider = new SimpleFilterProvider();
filterProvider.addFilter("excludeFieldFilter", new ExcludeField.Filter());
// 设置默认过滤器、防止未添加@JsonFilter的类报错
filterProvider.setDefaultFilter(com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter.serializeAll());
objectMapper.setFilterProvider(filterProvider);
return objectMapper;
}
}

View File

@ -0,0 +1,32 @@
package com.yj.earth.common.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
@Configuration
@EnableKnife4j
public class Knife4jConfig {
/**
* 自定义Swagger3文档信息
*/
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
// 文档基本信息
.info(new Info()
.title("最新产品API文档")
.description("远界大数据最新产品API文档【默认账号:admin、密码:admin123】")
.version("v1.0.0")
.contact(new Contact()
.name("周志雄"))
.license(new License()
.name("Apache 2.0")
.url("https://www.apache.org/licenses/LICENSE-2.0.html")));
}
}

View File

@ -0,0 +1,37 @@
package com.yj.earth.common.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class MyMetaObjectConfig implements MetaObjectHandler {
// 插入时自动填充
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createdAt", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updatedAt", LocalDateTime.class, LocalDateTime.now());
}
// 更新时自动填充
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updatedAt", LocalDateTime.class, LocalDateTime.now());
}
// 配置分页拦截器
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页拦截器
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}

View File

@ -0,0 +1,41 @@
package com.yj.earth.common.config;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.stp.StpUtil;
import com.yj.earth.common.exception.UnAuthException;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.ArrayList;
import java.util.List;
@Configuration
public class SaTokenConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
List<String> excludePathPatterns = new ArrayList<>();
excludePathPatterns.add("/user/login");
excludePathPatterns.add("/user/add");
excludePathPatterns.add("/doc.html");
excludePathPatterns.add("/webjars/**");
excludePathPatterns.add("/v3/api-docs/**");
excludePathPatterns.add("/fileInfo/download/**");
excludePathPatterns.add("/fileInfo/preview/**");
excludePathPatterns.add("/data/clt/**");
excludePathPatterns.add("/data/mbtiles/**");
excludePathPatterns.add("/data/pak/**");
// 注册 Sa-Token 拦截器
registry.addInterceptor(new SaInterceptor(handle -> {
// 登录校验
try {
StpUtil.checkLogin();
} catch (Exception e) {
throw new UnAuthException("未携带登录凭证");
}
})).addPathPatterns("/**")
.excludePathPatterns(excludePathPatterns);
}
}

View File

@ -0,0 +1,18 @@
package com.yj.earth.common.config;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Data
@Component
public class ServerConfig {
@Value("${server.port}")
private int port;
@Value("${server.host}")
private String host;
@Value("${sdk.port}")
private int sdkPort;
}

View File

@ -0,0 +1,29 @@
package com.yj.earth.common.config;
import com.yj.earth.annotation.SourceType;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import java.util.HashSet;
import java.util.Set;
@Configuration
public class SourceTypeConfig {
private static final String PACKAGE = "com.yj.earth.params";
@Bean
public Set<Class<?>> sourceParamClasses() throws ClassNotFoundException {
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AnnotationTypeFilter(SourceType.class));
Set<Class<?>> classes = new HashSet<>();
for (var beanDefinition : scanner.findCandidateComponents(PACKAGE)) {
classes.add(Class.forName(beanDefinition.getBeanClassName()));
}
return classes;
}
}

View File

@ -0,0 +1,14 @@
package com.yj.earth.common.constant;
public class GlobalConstant {
// 目录类型
public static final String DIRECTORY = "directory";
// 显示
public static final Integer SHOW = 1;
// 隐藏
public static final Integer HIDE = 0;
// SDK路径
public static final String SDKPATH = "sdk/geographysdk.jar";
// SDK日志路径
public static final String SDKLOG = "logs/sdk.log";
}

View File

@ -0,0 +1,170 @@
package com.yj.earth.common.core;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* 封装一个 StringRedisTemplate 的功能
*/
@Component
public class MapRedisTemplate {
// 底层存储结构、使用HashMap模拟Redis
private final Map<String, String> storage = new HashMap<>();
// 用于存储过期时间
private final Map<String, Long> expirationMap = new HashMap<>();
/**
* 获取操作字符串的接口
*/
public ValueOperations opsForValue() {
return new ValueOperations();
}
/**
* 删除指定的键
* @param key 要删除的键
* @return 是否删除成功
*/
public Boolean delete(String key) {
checkExpiration(key);
return storage.remove(key) != null;
}
/**
* 检查键是否存在
* @param key 要检查的键
* @return 键是否存在
*/
public Boolean hasKey(String key) {
checkExpiration(key);
return storage.containsKey(key);
}
/**
* 设置键的过期时间
* @param key 键
* @param timeout 过期时间
* @param unit 时间单位
* @return 是否设置成功
*/
public Boolean expire(String key, long timeout, TimeUnit unit) {
if (!storage.containsKey(key)) {
return false;
}
long expirationTime = System.currentTimeMillis() + unit.toMillis(timeout);
expirationMap.put(key, expirationTime);
return true;
}
/**
* 获取键的剩余过期时间
* @param key 键
* @param unit 时间单位
* @return 剩余过期时间
*/
public Long getExpire(String key, TimeUnit unit) {
checkExpiration(key);
if (!expirationMap.containsKey(key)) {
return -1L; // 永久有效
}
long remainingMillis = expirationMap.get(key) - System.currentTimeMillis();
if (remainingMillis <= 0) {
delete(key);
return 0L;
}
return unit.convert(remainingMillis, TimeUnit.MILLISECONDS);
}
/**
* 检查键是否过期、如果过期则删除
*/
private void checkExpiration(String key) {
if (expirationMap.containsKey(key)) {
long expirationTime = expirationMap.get(key);
if (System.currentTimeMillis() > expirationTime) {
storage.remove(key);
expirationMap.remove(key);
}
}
}
/**
* 操作字符串的内部类、模拟ValueOperations
*/
public class ValueOperations {
/**
* 设置键值对
* @param key 键
* @param value 值
*/
public void set(String key, String value) {
storage.put(key, value);
// 设置值时清除过期时间、模拟Redis行为
expirationMap.remove(key);
}
/**
* 设置键值对并指定过期时间
* @param key 键
* @param value 值
* @param timeout 过期时间
* @param unit 时间单位
*/
public void set(String key, String value, long timeout, TimeUnit unit) {
storage.put(key, value);
long expirationTime = System.currentTimeMillis() + unit.toMillis(timeout);
expirationMap.put(key, expirationTime);
}
/**
* 获取键对应的值
* @param key 键
* @return 对应的值
*/
public String get(String key) {
checkExpiration(key);
return storage.get(key);
}
/**
* 如果键不存在则设置值
* @param key 键
* @param value 值
* @return 是否设置成功
*/
public Boolean setIfAbsent(String key, String value) {
checkExpiration(key);
if (!storage.containsKey(key)) {
storage.put(key, value);
return true;
}
return false;
}
/**
* 自增操作
* @param key 键
* @return 自增后的值
*/
public Long increment(String key) {
return increment(key, 1);
}
/**
* 增加指定的值
* @param key 键
* @param delta 要增加的值
* @return 增加后的值
*/
public Long increment(String key, long delta) {
checkExpiration(key);
String value = storage.get(key);
long num = value == null ? 0 : Long.parseLong(value);
num += delta;
storage.put(key, String.valueOf(num));
return num;
}
}
}

View File

@ -0,0 +1,10 @@
package com.yj.earth.common.exception;
public class UnAuthException extends RuntimeException{
/**
* 带异常信息的构造方法
*/
public UnAuthException(String message) {
super(message);
}
}

View File

@ -0,0 +1,67 @@
package com.yj.earth.common.service;
import cn.hutool.crypto.digest.BCrypt;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.yj.earth.business.domain.Role;
import com.yj.earth.business.domain.Source;
import com.yj.earth.business.domain.User;
import com.yj.earth.business.service.RoleService;
import com.yj.earth.business.service.SourceService;
import com.yj.earth.business.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Slf4j
@Service
public class ServerInitService {
@Resource
private SourceService sourceService;
@Resource
private UserService userService;
@Resource
private RoleService roleService;
public void init() {
// 查询数据库所有需要加载的资源
List<Source> list =sourceService.list(new LambdaQueryWrapper<Source>()
.eq(Source::getSourceType, "terrain")
.or().eq(Source::getSourceType, "layer")
.or().eq(Source::getSourceType, "tileset"));
// 依次初始化
for (Source source : list) {
// 同步资源
sourceService.getDetail(source.getSourcePath(), sourceService.addAndGetSourceId(source.getSourcePath()));
log.info("初始化资源<--{}-->完成", source.getSourceName());
}
}
public void checkDefaultData() {
// 查询角色表和用户表是否有数据
if(roleService.count() == 0 && userService.count() == 0) {
log.info("初始化默认数据");
// 新增一个管理员角色
Role adminRole = new Role();
adminRole.setRoleName("管理员");
adminRole.setDescription("系统管理员");
adminRole.setIsSuper(1);
roleService.save(adminRole);
// 新增一个默认角色
Role defaultRole = new Role();
defaultRole.setRoleName("默认角色");
defaultRole.setDescription("系统默认角色");
defaultRole.setIsSuper(0);
roleService.save(defaultRole);
// 新增一个用户
User user = new User();
user.setUsername("admin");
user.setPassword(BCrypt.hashpw("admin123", BCrypt.gensalt()));
user.setNickname("管理员");
user.setRoleId(adminRole.getId());
user.setPhone("13888888888");
userService.save(user);
}
}
}

View File

@ -0,0 +1,57 @@
package com.yj.earth.common.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yj.earth.annotation.SourceType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@Component
public class SourceParamsValidator {
private final ObjectMapper objectMapper;
private final Map<String, Class<?>> sourceTypeMap = new HashMap<>();
@Autowired
public SourceParamsValidator(ObjectMapper objectMapper, Set<Class<?>> sourceParamClasses) {
this.objectMapper = objectMapper;
// 初始化资源类型与参数类的映射关系
for (Class<?> clazz : sourceParamClasses) {
SourceType annotation = clazz.getAnnotation(SourceType.class);
if (annotation != null) {
sourceTypeMap.put(annotation.value(), clazz);
}
}
}
/**
* 验证并转换参数
*/
public Object validateAndConvert(String sourceType, Map<String, Object> params) {
// 检查是否有对应的参数类
Class<?> paramClass = sourceTypeMap.get(sourceType);
if (paramClass == null) {
String message = "不支持 " + sourceType + "的资源类型";
throw new IllegalArgumentException(message);
}
// 转换并验证参数
try {
return objectMapper.convertValue(params, paramClass);
} catch (IllegalArgumentException e) {
String message = "请核对类型和参数";
throw new IllegalArgumentException(message);
}
}
/**
* 获取所有支持的资源类型
*/
public Set<String> getSupportedSourceTypes() {
return sourceTypeMap.keySet();
}
}

View File

@ -0,0 +1,66 @@
package com.yj.earth.common.util;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* AES对称加密工具类
*/
public class AesEncryptUtil {
/**
* AES加密
* @param content 待加密内容
* @param key 密钥16位/24位/32位、对应AES-128/AES-192/AES-256
* @param algorithm 加密算法如AES/CBC/PKCS5Padding
* @return 加密后的Base64字符串
*/
public static String encrypt(String content, String key, String algorithm) {
try {
// 创建密钥
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");
// 初始化加密器
Cipher cipher = Cipher.getInstance(algorithm);
// 如果是CBC模式、需要初始化向量IV与密钥同长度
if (algorithm.contains("CBC")) {
IvParameterSpec iv = new IvParameterSpec(key.substring(0, 16).getBytes(StandardCharsets.UTF_8));
cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);
} else {
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
}
// 加密并转为Base64
byte[] encrypted = cipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) {
throw new RuntimeException("AES加密失败", e);
}
}
/**
* 解密方法(如果需要解密可以实现)
*/
public static String decrypt(String encryptedContent, String key, String algorithm) {
try {
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");
Cipher cipher = Cipher.getInstance(algorithm);
if (algorithm.contains("CBC")) {
IvParameterSpec iv = new IvParameterSpec(key.substring(0, 16).getBytes(StandardCharsets.UTF_8));
cipher.init(Cipher.DECRYPT_MODE, secretKey, iv);
} else {
cipher.init(Cipher.DECRYPT_MODE, secretKey);
}
byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(encryptedContent));
return new String(decrypted, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("AES解密失败", e);
}
}
}

View File

@ -0,0 +1,56 @@
package com.yj.earth.common.util;
import lombok.Data;
@Data
public class ApiResponse<T> {
private int code; // 状态码
private T data; // 响应数据
private String message; // 响应消息
// 私有化构造方法
private ApiResponse(int code, T data, String message) {
this.code = code;
this.data = data;
this.message = message;
}
// 成功响应(带数据)
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(200, data, "操作成功");
}
// 成功响应(无数据)
public static <T> ApiResponse<T> successWithMessage(String message) {
return new ApiResponse<>(200, null, message);
}
// 失败响应(带自定义消息)
public static <T> ApiResponse<T> failure(String message) {
return new ApiResponse<>(20000, null, message);
}
// 失败响应(未授权)
public static <T> ApiResponse<T> failureWithNoAuth(String message) {
return new ApiResponse<>(401, null, message);
}
// 设置 data 字段、并返回当前对象、支持链式调用
public ApiResponse<T> setData(T data) {
this.data = data;
return this;
}
// 设置 message 字段、并返回当前对象、支持链式调用
public ApiResponse<T> setMessage(String message) {
this.message = message;
return this;
}
// 设置 code 字段、并返回当前对象、支持链式调用
public ApiResponse<T> setCode(int code) {
this.code = code;
return this;
}
}

View File

@ -0,0 +1,86 @@
package com.yj.earth.common.util;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.fill.Column;
import com.yj.earth.datasource.DatabaseManager;
import java.io.File;
import java.util.Collections;
public class CodeUtil {
// SQLite数据库配置
private static String databasePath = null;
private static String author = "周志雄";
public static void main(String[] args) {
DatabaseManager.initDatabase(DatabaseManager.DatabaseType.SQLITE);
databasePath = DatabaseManager.getSqliteDbFilePath();
// 检查数据库路径是否有效
if (databasePath == null || databasePath.trim().isEmpty()) {
throw new RuntimeException("数据库路径未正确初始化");
}
// 确保数据库目录存在
File dbFile = new File(databasePath);
File parentDir = dbFile.getParentFile();
if (!parentDir.exists()) {
parentDir.mkdirs();
}
// 传入需要生成代码的表名
Generation("file_info");
}
public static void Generation(String... tableName) {
// 构建SQLite连接URL
String jdbcUrl = "jdbc:sqlite:" + databasePath;
// FastAutoGenerator 用来创建代码生成器实例
FastAutoGenerator.create(jdbcUrl, "", "")
.globalConfig(builder -> {
builder.author(author)
.enableSpringdoc()
.outputDir(System.getProperty("user.dir") + "/src/main/java");
}).packageConfig(builder -> {
builder.entity("domain")
.parent("com.yj.earth.business")
.controller("controller")
.mapper("mapper")
.service("service")
.serviceImpl("service.impl")
.pathInfo(Collections.singletonMap(OutputFile.xml,
System.getProperty("user.dir") + "/src/main/resources/mapper"));
}).strategyConfig(builder -> {
builder.addInclude(tableName)
.addTablePrefix("t_")
.entityBuilder()
.enableLombok()
.enableChainModel()
.addTableFills(new Column("created_at", FieldFill.INSERT),
new Column("updated_at", FieldFill.UPDATE))
.naming(NamingStrategy.underline_to_camel)
.columnNaming(NamingStrategy.underline_to_camel)
.idType(IdType.ASSIGN_UUID)
.formatFileName("%s")
.mapperBuilder()
.enableMapperAnnotation()
.enableBaseResultMap()
.enableBaseColumnList()
.formatMapperFileName("%sMapper")
.formatXmlFileName("%sMapper")
.serviceBuilder()
.formatServiceFileName("%sService")
.formatServiceImplFileName("%sServiceImpl")
.controllerBuilder()
.enableRestStyle()
.formatFileName("%sController")
.enableHyphenStyle();
}).execute();
}
}

View File

@ -0,0 +1,180 @@
package com.yj.earth.common.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* HTTP 请求工具类
*/
@Slf4j
public class HttpUtil {
// 编码格式
private static final String CHARSET = "UTF-8";
// 连接超时时间 5 秒
private static final int CONNECT_TIMEOUT = 5000;
// 读取超时时间 10 秒
private static final int READ_TIMEOUT = 10000;
// JSON 处理器
private static final ObjectMapper objectMapper = new ObjectMapper();
/**
* 发送 GET 请求
*/
public static String doGet(String url) {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpGet httpGet = new HttpGet(url);
// 设置超时配置
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(CONNECT_TIMEOUT)
.setSocketTimeout(READ_TIMEOUT)
.build();
httpGet.setConfig(requestConfig);
try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
return handleResponse(response);
}
} catch (Exception e) {
log.error("GET 请求发生异常、请求 URL: {}", url, e);
return null;
}
}
/**
* 发送表单参数的 POST 请求
* 参数类型改为 Map<String, Object> 以支持更多类型的参数值
*/
public static String doPostForm(String url, Map<String, Object> params) {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpPost httpPost = new HttpPost(url);
// 设置超时配置
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(CONNECT_TIMEOUT)
.setSocketTimeout(READ_TIMEOUT)
.build();
httpPost.setConfig(requestConfig);
// 组装表单参数
if (params != null && !params.isEmpty()) {
List<NameValuePair> nameValuePairs = new ArrayList<>();
for (Map.Entry<String, Object> entry : params.entrySet()) {
// 将 Object 类型的值转换为字符串
String value = entry.getValue() != null ? entry.getValue().toString() : null;
nameValuePairs.add(new BasicNameValuePair(entry.getKey(), value));
}
httpPost.setEntity(new UrlEncodedFormEntity(nameValuePairs, CHARSET));
}
// 执行请求
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
return handleResponse(response);
}
} catch (Exception e) {
log.error("表单 POST 请求发生异常、请求 URL: {}、请求参数: {}", url, params, e);
return null;
}
}
/**
* 发送 JSON 参数的 POST 请求
* 参数类型统一为 Map<String, Object>
*/
public static String doPostJson(String url, Map<String, Object> params) {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpPost httpPost = new HttpPost(url);
// 设置超时配置
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(CONNECT_TIMEOUT)
.setSocketTimeout(READ_TIMEOUT)
.build();
httpPost.setConfig(requestConfig);
// 设置 JSON 请求头
httpPost.setHeader("Content-Type", "application/json;charset=" + CHARSET);
// Map 转 JSON 字符串并设置为请求体
if (params != null && !params.isEmpty()) {
String jsonParams = objectMapper.writeValueAsString(params);
httpPost.setEntity(new StringEntity(jsonParams, CHARSET));
}
// 执行请求
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
return handleResponse(response);
}
} catch (JsonProcessingException e) {
log.error("JSON POST 请求参数序列化失败、请求 URL: {}、请求参数: {}", url, params, e);
} catch (Exception e) {
log.error("JSON POST 请求发生异常、请求 URL: {}、请求参数: {}", url, params, e);
}
return null;
}
/**
* 发送 GET 请求、返回字节数组的ResponseEntity、适用于下载文件
*/
public static ResponseEntity<byte[]> doGetForByteArrayResponse(String url) {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpGet httpGet = new HttpGet(url);
// 设置超时配置
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(CONNECT_TIMEOUT)
.setSocketTimeout(READ_TIMEOUT)
.build();
httpGet.setConfig(requestConfig);
try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
// 获取状态码
int statusCode = response.getStatusLine().getStatusCode();
HttpStatus httpStatus = HttpStatus.valueOf(statusCode);
// 处理响应头
HttpHeaders headers = new HttpHeaders();
Header[] allHeaders = response.getAllHeaders();
for (Header header : allHeaders) {
// 特别保留Content-Disposition和Content-Type、用于前端下载
headers.add(header.getName(), header.getValue());
}
// 处理响应体(二进制数据)
HttpEntity entity = response.getEntity();
byte[] body = entity != null ? EntityUtils.toByteArray(entity) : null;
// 返回ResponseEntity对象
return new ResponseEntity<>(body, headers, httpStatus);
}
} catch (Exception e) {
log.error("GET 下载请求发生异常、请求 URL: {}", url, e);
// 发生异常时返回500错误
return new ResponseEntity<>(e.getMessage().getBytes(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
/**
* 通用响应处理方法
*/
private static String handleResponse(CloseableHttpResponse response) throws IOException {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
HttpEntity entity = response.getEntity();
return entity != null ? EntityUtils.toString(entity, CHARSET) : null;
}
log.warn("HTTP 请求失败、状态码: {}、响应内容: {}", statusCode, response.getEntity() != null ? EntityUtils.toString(response.getEntity(), CHARSET) : null);
return null;
}
}

View File

@ -0,0 +1,50 @@
package com.yj.earth.common.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Map;
/**
* Map与JSON互相转换工具类
*/
@Slf4j
public class JsonMapConverter {
private static final ObjectMapper objectMapper = new ObjectMapper();
/**
* 将 Map 转换为 JSON 字符串
*/
public static String mapToJson(Map<String, Object> map) {
if (map == null || map.isEmpty()) {
return "{}";
}
try {
return objectMapper.writeValueAsString(map);
} catch (JsonProcessingException e) {
log.error("Map转JSON失败", e);
return null;
}
}
/**
* 将 JSON 字符串转换为 Map
*/
public static Map<String, Object> jsonToMap(String json) {
if (json == null || json.trim().isEmpty()) {
return new HashMap<>(0);
}
try {
return objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {});
} catch (Exception e) {
log.error("JSON转Map失败、JSON内容: {}", json, e);
return new HashMap<>(0);
}
}
}

View File

@ -0,0 +1,116 @@
package com.yj.earth.common.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashMap;
import java.util.Map;
public class MapUtil {
private static final ObjectMapper objectMapper = new ObjectMapper();
/**
* 合并两个 Map、如果存在相同的 Key、则用新 Map 中的值替换原始 Map 中的值
* 如果原始 Map 中有某个 Key 而新 Map 中没有、则保持原始 Map 中的值不变
*/
public static <K, V> void mergeMaps(Map<K, V> originalMap, Map<K, V> newMap) {
// 检查参数是否为null、避免空指针异常
if (originalMap == null || newMap == null) {
throw new IllegalArgumentException("参数Map不能为null");
}
// 遍历新Map中的所有键值对
for (Map.Entry<K, V> entry : newMap.entrySet()) {
K key = entry.getKey();
// 如果原始Map中存在相同的key、则替换值
if (originalMap.containsKey(key)) {
originalMap.put(key, entry.getValue());
}
// 如果原始Map中不存在该key、则不做任何操作
}
}
/**
* 将JSON字符串转换为Map对象
*
* @param jsonString JSON格式的字符串
* @return 转换后的Map对象、如果JSON为空则返回空Map
* @throws IllegalArgumentException 当JSON字符串无效或解析失败时抛出
*/
public static <K, V> Map<K, V> jsonToMap(String jsonString) {
if (jsonString == null || jsonString.trim().isEmpty()) {
return new HashMap<>();
}
try {
// 将JSON字符串转换为Map
return objectMapper.readValue(jsonString, Map.class);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("JSON字符串解析失败: " + e.getMessage(), e);
}
}
/**
* 将 Map 对象转换为JSON字符串
*/
public static <K, V> String mapToString(Map<K, V> map) {
if (map == null) {
return "";
}
try {
// 将Map转换为JSON字符串
return objectMapper.writeValueAsString(map);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Map转换为JSON字符串失败: " + e.getMessage(), e);
}
}
/**
* 将任意类型对象转换为JSON字符串
*/
public static <T> String objectToJson(T object) {
if (object == null) {
return "";
}
try {
// 将对象转换为JSON字符串
return objectMapper.writeValueAsString(object);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("对象转换为JSON字符串失败: " + e.getMessage(), e);
}
}
/**
* 从Map中获取指定key的字符串值
*
* @param map 数据源Map
* @param key 要获取的字段名
* @return 字段的字符串值、如果map为null或key不存在则返回null
*/
public static String getString(Map<?, ?> map, String key) {
if (map == null || key == null) {
return null;
}
Object value = map.get(key);
return value != null ? value.toString() : null;
}
/**
* 直接从JSON字符串中获取指定key的字符串值
*
* @param jsonString JSON格式的字符串
* @param key 要获取的字段名
* @return 字段的字符串值、如果JSON为空或key不存在则返回null
*/
public static String getString(String jsonString, String key) {
if (jsonString == null || key == null) {
return null;
}
Map<?, ?> map = jsonToMap(jsonString);
return getString(map, key);
}
}

View File

@ -0,0 +1,114 @@
package com.yj.earth.common.util;
import lombok.extern.slf4j.Slf4j;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@Slf4j
public class PortKillUtil {
/**
* 根据端口号杀死对应的进程
*/
public static boolean killProcessByPort(int port) {
// 获取操作系统类型
String osName = System.getProperty("os.name").toLowerCase();
try {
if (osName.contains("windows")) {
// Windows系统处理逻辑
return killWindowsProcess(port);
} else if (osName.contains("linux") || osName.contains("unix")) {
// Linux/Unix系统处理逻辑
return killLinuxProcess(port);
} else {
log.error("不支持的操作系统: " + osName);
return false;
}
} catch (Exception e) {
log.error("杀死进程时发生错误: " + e.getMessage(), e);
return false;
}
}
/**
* 杀死 Windows 系统中占用指定端口的进程
*/
private static boolean killWindowsProcess(int port) throws IOException, InterruptedException {
// 查找占用端口的进程ID
Process process = Runtime.getRuntime().exec("netstat -ano | findstr :" + port);
process.waitFor();
// 读取命令输出
String pid = getWindowsPidFromOutput(process.getInputStream());
if (pid == null || pid.isEmpty()) {
log.error("端口 " + port + " 未被占用");
return true;
}
// 杀死找到的进程
Process killProcess = Runtime.getRuntime().exec("taskkill /F /PID " + pid);
int exitCode = killProcess.waitFor();
if (exitCode == 0) {
log.info("成功杀死端口 " + port + " 对应的进程、PID: " + pid);
return true;
} else {
log.error("杀死端口 " + port + " 对应的进程失败、PID: " + pid);
return false;
}
}
/**
* 从 Windows 命令输出中提取进程 ID
*/
private static String getWindowsPidFromOutput(InputStream inputStream) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
// 查找包含LISTENING状态的行
if (line.contains("LISTENING")) {
// 提取最后一个空格后的数字作为PID
String[] parts = line.split("\\s+");
return parts[parts.length - 1];
}
}
}
return null;
}
/**
* 杀死 Linux 系统中占用指定端口的进程
*/
private static boolean killLinuxProcess(int port) throws IOException, InterruptedException {
// 查找占用端口的进程ID
Process process = Runtime.getRuntime().exec("lsof -i:" + port + " -t");
process.waitFor();
// 读取命令输出获取PID
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String pid = reader.readLine();
if (pid == null || pid.isEmpty()) {
log.error("端口 " + port + " 未被占用");
return true;
}
// 杀死找到的进程
Process killProcess = Runtime.getRuntime().exec("kill -9 " + pid);
int exitCode = killProcess.waitFor();
if (exitCode == 0) {
log.info("成功杀死端口 " + port + " 对应的进程、PID: " + pid);
return true;
} else {
log.error("杀死端口 " + port + " 对应的进程失败、PID: " + pid);
return false;
}
}
}
}

View File

@ -0,0 +1,33 @@
package com.yj.earth.common.util;
public class PositionUtil {
private static final double a = 6378137.0; // 椭球长半轴
private static final double e = 0.0818191908426; // 椭球第一偏心率
private static final double epsilon = 1e-8; // 迭代精度
private static final double r2d = 180.0 / Math.PI; // 弧度转角度
public static double[] xyz2Blh(double x, double y, double z) {
double tmpX = x;
double tmpY = y;
double tmpZ = z;
double curB = 0.0;
double N = 0.0;
double calB = Math.atan2(tmpZ, Math.sqrt(tmpX * tmpX + tmpY * tmpY));
int counter = 0;
while (Math.abs(curB - calB) * r2d > epsilon && counter < 25) {
curB = calB;
N = a / Math.sqrt(1 - e * e * Math.sin(curB) * Math.sin(curB));
calB = Math.atan2(tmpZ + N * e * e * Math.sin(curB),
Math.sqrt(tmpX * tmpX + tmpY * tmpY));
counter++;
}
double longitude = Math.atan2(tmpY, tmpX) * r2d;
double latitude = curB * r2d;
double height = tmpZ / Math.sin(curB) - N * (1 - e * e);
return new double[]{longitude, latitude, height};
}
}

View File

@ -0,0 +1,126 @@
package com.yj.earth.common.util;
import com.yj.earth.common.constant.GlobalConstant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.yaml.snakeyaml.Yaml;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Slf4j
public class SdkUtil {
// 保存SDK进程引用
private static Process sdkProcess;
// 保存SDK端口号、用于关闭时强制终止
private static Integer sdkPort;
// 对外提供的启动入口
public static void startSdkIfConfigured() throws IOException {
// 读取配置
sdkPort = getSdkPortFromYamlConfig();
// 未配置则不启动
if (sdkPort == null) {
log.info("请先配置SDK端口");
return;
}
// 配置存在时、正常启动SDK
startSdkJar(sdkPort);
}
// 接收已确认的端口、启动SDK
private static void startSdkJar(int sdkPort) throws IOException {
// 获取项目根目录(当前工作目录)
String projectRoot = System.getProperty("user.dir");
// 获取SDK完整路径
String sdkJarPath = new File(projectRoot, GlobalConstant.SDKPATH).getAbsolutePath();
// 校验SDK
File sdkJarFile = new File(sdkJarPath);
if (!sdkJarFile.exists() || !sdkJarFile.isFile()) {
log.error("SDK不存在或不是有效文件:{}", sdkJarPath);
}
log.info("准备启动SDK: {}", sdkJarPath);
log.info("使用SDK端口: {}", sdkPort);
// 构建启动命令、添加 -Dserver.port 参数
List<String> command = new ArrayList<>();
command.add("java");
command.add("-Dserver.port=" + sdkPort);
command.add("-jar");
command.add(sdkJarPath);
// 构建进程启动器
ProcessBuilder processBuilder = new ProcessBuilder(command);
// 打印执行的命令
String commandStr = command.stream().collect(Collectors.joining(" "));
log.info("执行命令: {}", commandStr);
// 输出SDK的控制台日志到当前应用的日志中
processBuilder.redirectErrorStream(true);
// 日志文件路径建议优化: 避免与项目根目录混淆
File sdkLogFile = new File(projectRoot, GlobalConstant.SDKLOG);
// 确保目录存在(避免日志写入失败)
if (!sdkLogFile.getParentFile().exists()) {
sdkLogFile.getParentFile().mkdirs();
}
processBuilder.redirectOutput(sdkLogFile);
// 启动进程(非阻塞)
sdkProcess = processBuilder.start();
log.info("SDK已在后台启动、进程ID: {}", sdkProcess.pid());
// 注册JVM关闭钩子、在主程序退出时关闭SDK进程
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
if (sdkProcess != null && sdkProcess.isAlive()) {
log.info("主程序关闭、正在停止SDK进程PID: {}...", sdkProcess.pid());
// 销毁子进程
sdkProcess.destroy();
try {
// 等待进程终止最多5秒
boolean terminated = sdkProcess.waitFor(5, TimeUnit.SECONDS);
if (terminated) {
log.info("SDK进程已成功停止");
} else {
log.warn("SDK进程未能正常停止、尝试通过端口{}强制终止...", sdkPort);
// 通过端口强制终止
boolean killSuccess = PortKillUtil.killProcessByPort(sdkPort);
if (killSuccess) {
log.info("已通过端口{}强制终止SDK进程", sdkPort);
} else {
log.error("通过端口{}强制终止SDK进程失败", sdkPort);
}
}
} catch (InterruptedException e) {
log.error("停止SDK进程时发生中断", e);
Thread.currentThread().interrupt();
}
}
}, "SDK-Process-Shutdown-Hook"));
}
/**
* 从配置文件读取SDK端口配置
*/
private static Integer getSdkPortFromYamlConfig() {
Yaml yaml = new Yaml();
try (InputStream inputStream = new ClassPathResource("application.yml").getInputStream()) {
// 解析YAML文件为Map
Map<String, Object> yamlMap = yaml.load(inputStream);
// 逐级获取配置
if (yamlMap.containsKey("sdk")) {
Object sdkObj = yamlMap.get("sdk");
if (sdkObj instanceof Map) {
Map<?, ?> sdkMap = (Map<?, ?>) sdkObj;
if (sdkMap.containsKey("port")) {
return ((Number) sdkMap.get("port")).intValue();
}
}
}
log.error("未配置SDK端口");
} catch (IOException e) {
log.error("读取配置文件失败", e);
}
return null;
}
}

View File

@ -0,0 +1,80 @@
package com.yj.earth.common.util;
import lombok.extern.slf4j.Slf4j;
import oshi.SystemInfo;
import oshi.hardware.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.HexFormat;
/**
* 服务器唯一标识工具类
* 基于CPU、主板、磁盘、网卡硬件信息生成唯一标识、不更换核心硬件则标识不变
*/
@Slf4j
public class ServerUniqueIdUtil {
/**
* 获取服务器唯一标识
*/
public static String getServerUniqueId() {
// 初始化系统信息oshi核心入口
SystemInfo systemInfo = new SystemInfo();
HardwareAbstractionLayer hardware = systemInfo.getHardware();
try {
// 收集稳定的核心硬件信息
StringBuilder hardwareRawInfo = new StringBuilder();
// CPU唯一标识
CentralProcessor cpu = hardware.getProcessor();
String cpuId = cpu.getProcessorIdentifier().getProcessorID();
if (cpuId != null && !cpuId.trim().isEmpty()) {
hardwareRawInfo.append(cpuId).append("|");
}
// 主板UUID
ComputerSystem mainBoard = hardware.getComputerSystem();
String boardUuid = mainBoard.getHardwareUUID();
if (boardUuid != null && !boardUuid.trim().isEmpty()) {
hardwareRawInfo.append(boardUuid).append("|");
}
// 第一个物理磁盘序列号
List<HWDiskStore> disks = hardware.getDiskStores();
for (HWDiskStore disk : disks) {
// 过滤虚拟磁盘(
if (!disk.getModel().toLowerCase().contains("virtual") && disk.getSize() > 0) {
String diskSerial = disk.getSerial();
if (diskSerial != null && !diskSerial.trim().isEmpty()) {
hardwareRawInfo.append(diskSerial).append("|");
break;
}
}
}
// 第一个物理网卡MAC地址
List<NetworkIF> netCards = hardware.getNetworkIFs();
for (NetworkIF netCard : netCards) {
String mac = netCard.getMacaddr();
// 过滤条件非空、非全零MAC、非回环网卡
if (mac != null && !mac.trim().isEmpty() && !mac.startsWith("00:00:00:00:00:00")) {
hardwareRawInfo.append(mac).append("|");
break;
}
}
// MD5哈希
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] hashBytes = md.digest(hardwareRawInfo.toString().getBytes());
// 字节数组转十六进制字符串
return HexFormat.of().formatHex(hashBytes).toUpperCase();
} catch (Exception e) {
return "unknown";
}
}
}

View File

@ -0,0 +1,401 @@
package com.yj.earth.datasource;
import com.yj.earth.design.*;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.yaml.snakeyaml.Yaml;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@Data
@Slf4j
public class DatabaseManager {
public enum DatabaseType {
SQLITE, MYSQL
}
private static final String EXCLUDE_SERIAL_FIELD = "serialVersionUID";
private static final List<Class<?>> ENTITY_CLASSES;
private static final String FOLDER_NAME = "yjearth";
private static final String DB_FILE_NAME = "app.db";
private static String sqliteDbFilePath;
private static boolean isSqliteInitialized = false;
private static String mysqlUrl;
private static String mysqlUsername;
private static String mysqlPassword;
private static boolean isMysqlInitialized = false;
private static final String SQLITE_DRIVER = "org.sqlite.JDBC";
private static final String MYSQL_DRIVER = "com.mysql.cj.jdbc.Driver";
private static final String MYSQL_TABLE_ENGINE = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
static {
List<Class<?>> classes = new ArrayList<>();
classes.add(User.class);
classes.add(Role.class);
classes.add(Source.class);
classes.add(RoleSource.class);
classes.add(FileInfo.class);
ENTITY_CLASSES = Collections.unmodifiableList(classes);
}
public static void initDatabase(DatabaseType dbType) {
if (dbType == null) {
log.error("数据库类型不能为空");
return;
}
if (isInitialized(dbType)) {
log.info("{}数据库已初始化、无需重复执行", dbType.name());
return;
}
try {
loadConfig(dbType);
if (!validateConfig(dbType)) {
log.error("{}数据库配置无效、初始化失败", dbType.name());
return;
}
loadDriver(dbType);
preProcess(dbType);
createTablesForEntities(dbType);
markInitialized(dbType, true);
log.info("{}数据库初始化成功({}", dbType.name(), getInitSuccessMsg(dbType));
} catch (ClassNotFoundException e) {
log.error("{}驱动类未找到、请检查依赖: {}", dbType.name(), e.getMessage(), e);
} catch (SQLException e) {
log.error("{}数据库操作失败: {}", dbType.name(), e.getMessage(), e);
} catch (Exception e) {
log.error("{}数据库初始化未知异常: {}", dbType.name(), e.getMessage(), e);
}
}
public static String getUnderlineName(String camelCaseName) {
if (camelCaseName == null || camelCaseName.isEmpty()) {
throw new IllegalArgumentException("命名转换的入参名称不能为空");
}
StringBuilder underlineName = new StringBuilder();
underlineName.append(Character.toLowerCase(camelCaseName.charAt(0)));
for (int i = 1; i < camelCaseName.length(); i++) {
char currentChar = camelCaseName.charAt(i);
if (Character.isUpperCase(currentChar)) {
underlineName.append("_").append(Character.toLowerCase(currentChar));
} else {
underlineName.append(currentChar);
}
}
return underlineName.toString();
}
public static String getSqliteDbFilePath() {
return sqliteDbFilePath;
}
private static void loadConfig(DatabaseType dbType) throws IOException {
if (dbType != DatabaseType.MYSQL) return;
try (InputStream yamlInput = new ClassPathResource("application.yml").getInputStream()) {
Yaml yaml = new Yaml();
Map<String, Object> yamlData = yaml.load(yamlInput);
Map<String, Object> springMap = (Map<String, Object>) yamlData.get("spring");
Map<String, Object> datasourceMap = (Map<String, Object>) springMap.get("datasource");
Map<String, Object> mysqlMap = (Map<String, Object>) datasourceMap.get("mysql");
mysqlUrl = getConfigValue(mysqlMap, "url");
mysqlUsername = getConfigValue(mysqlMap, "username");
mysqlPassword = getConfigValue(mysqlMap, "password");
}
}
private static boolean validateConfig(DatabaseType dbType) {
if (dbType == DatabaseType.SQLITE) return true;
if (mysqlUrl == null || mysqlUrl.isEmpty()) {
log.error("MySQL配置缺失: spring.datasource.mysql.url");
return false;
}
if (mysqlUsername == null || mysqlUsername.isEmpty()) {
log.error("MySQL配置缺失: spring.datasource.mysql.username");
return false;
}
if (!mysqlUrl.startsWith("jdbc:mysql://")) {
log.error("MySQL URL格式错误、需以[jdbc:mysql://]开头、当前: {}", mysqlUrl);
return false;
}
return true;
}
private static void loadDriver(DatabaseType dbType) throws ClassNotFoundException {
if (dbType == DatabaseType.SQLITE) {
Class.forName(SQLITE_DRIVER);
log.info("SQLite驱动加载成功");
} else {
Class.forName(MYSQL_DRIVER);
log.info("MySQL驱动加载成功");
}
}
private static void preProcess(DatabaseType dbType) throws IOException {
if (dbType != DatabaseType.SQLITE) return;
Path systemCacheDir = getRecommendedCacheDirectory();
if (systemCacheDir == null) {
throw new IOException("无法获取有效的系统缓存目录、无法创建SQLite文件");
}
Path appDir = systemCacheDir.resolve(FOLDER_NAME);
Path dbFile = appDir.resolve(DB_FILE_NAME);
sqliteDbFilePath = dbFile.toAbsolutePath().toString();
if (!Files.exists(appDir)) {
Files.createDirectories(appDir);
log.info("创建SQLite应用目录: {}", appDir);
}
if (!Files.isWritable(appDir)) {
throw new IOException("无权限写入SQLite目录: " + appDir);
}
if (!Files.exists(dbFile)) {
Files.createFile(dbFile);
log.info("创建SQLite新文件: {}", sqliteDbFilePath);
} else {
log.info("SQLite文件已存在: {}", sqliteDbFilePath);
}
}
private static void createTablesForEntities(DatabaseType dbType) throws SQLException {
if (ENTITY_CLASSES.isEmpty()) {
log.warn("未配置需要创建表的实体类、跳过批量建表");
return;
}
try (Connection connection = getConnection(dbType)) {
for (Class<?> entityClass : ENTITY_CLASSES) {
createTableIfNotExists(connection, dbType, entityClass);
}
}
}
private static void createTableIfNotExists(Connection connection, DatabaseType dbType, Class<?> entityClass) throws SQLException {
String tableName = getUnderlineName(entityClass.getSimpleName());
if (isTableExists(connection, dbType, tableName)) {
log.info("{}表[{}]已存在、跳过创建", dbType.name(), tableName);
return;
}
String createSql = generateCreateTableSql(dbType, entityClass, tableName);
try (Statement statement = connection.createStatement()) {
statement.execute(createSql);
log.info("{}表[{}]创建成功、执行SQL: {}", dbType.name(), tableName, createSql);
}
}
private static Connection getConnection(DatabaseType dbType) throws SQLException {
if (dbType == DatabaseType.SQLITE) {
return DriverManager.getConnection("jdbc:sqlite:" + sqliteDbFilePath);
} else {
return DriverManager.getConnection(mysqlUrl, mysqlUsername, mysqlPassword);
}
}
private static boolean isTableExists(Connection connection, DatabaseType dbType, String tableName) throws SQLException {
if (dbType == DatabaseType.SQLITE) {
try (ResultSet rs = connection.getMetaData().getTables(null, null, tableName, new String[]{"TABLE"})) {
return rs.next();
}
} else {
String dbName = extractDbNameFromMysqlUrl(mysqlUrl);
String checkSql = "SELECT 1 FROM information_schema.tables WHERE table_schema = ? AND table_name = ? LIMIT 1";
try (PreparedStatement pstmt = connection.prepareStatement(checkSql)) {
pstmt.setString(1, dbName);
pstmt.setString(2, tableName);
try (ResultSet rs = pstmt.executeQuery()) {
return rs.next();
}
}
}
}
private static String generateCreateTableSql(DatabaseType dbType, Class<?> entityClass, String tableName) {
StringBuilder sqlBuilder = new StringBuilder("CREATE TABLE IF NOT EXISTS ").append(tableName).append(" (");
Field[] fields = entityClass.getDeclaredFields();
List<String> columnDefinitions = new ArrayList<>();
for (Field field : fields) {
if (EXCLUDE_SERIAL_FIELD.equals(field.getName()) || Modifier.isStatic(field.getModifiers())) {
continue;
}
field.setAccessible(true);
String columnName = getUnderlineName(field.getName());
String dbTypeStr = mapJavaTypeToDbType(dbType, field.getType());
StringBuilder columnDef = new StringBuilder(columnName).append(" ").append(dbTypeStr);
if ("id".equals(field.getName())) {
columnDef.append(dbType == DatabaseType.SQLITE ? " PRIMARY KEY" : " PRIMARY KEY AUTO_INCREMENT");
}
columnDefinitions.add(columnDef.toString());
}
for (int i = 0; i < columnDefinitions.size(); i++) {
sqlBuilder.append(columnDefinitions.get(i));
if (i != columnDefinitions.size() - 1) {
sqlBuilder.append(", ");
}
}
if (dbType == DatabaseType.MYSQL) {
sqlBuilder.append(") ").append(MYSQL_TABLE_ENGINE);
} else {
sqlBuilder.append(")");
}
return sqlBuilder.toString();
}
private static String mapJavaTypeToDbType(DatabaseType dbType, Class<?> javaType) {
return dbType == DatabaseType.SQLITE ? mapJavaTypeToSqlite(javaType) : mapJavaTypeToMysql(javaType);
}
private static String mapJavaTypeToSqlite(Class<?> javaType) {
if (javaType == int.class || javaType == Integer.class || javaType == long.class || javaType == Long.class || javaType == short.class || javaType == Short.class) {
return "INTEGER";
} else if (javaType == float.class || javaType == Float.class || javaType == double.class || javaType == Double.class) {
return "REAL";
} else if (javaType == boolean.class || javaType == Boolean.class) {
return "INTEGER";
} else if (javaType == String.class) {
return "TEXT";
} else if (javaType == byte[].class) {
return "BLOB";
} else {
return "TEXT";
}
}
private static Path getRecommendedCacheDirectory() {
String os = System.getProperty("os.name").toLowerCase();
if (os.contains("win")) {
String appData = System.getenv("APPDATA");
if (appData != null && !appData.isEmpty()) {
Path path = Paths.get(appData);
if (Files.exists(path) && Files.isWritable(path)) {
return path;
}
}
} else if (os.contains("nix") || os.contains("nux")) {
String userHome = System.getProperty("user.home");
if (userHome != null && !userHome.isEmpty()) {
Path path = Paths.get(userHome).resolve(".cache");
try {
if (!Files.exists(path)) {
Files.createDirectories(path);
}
if (Files.isWritable(path)) {
return path;
}
} catch (IOException e) {
log.error("无法访问Linux .cache目录: {}", e.getMessage(), e);
}
}
}
return null;
}
private static String mapJavaTypeToMysql(Class<?> javaType) {
if (javaType == int.class || javaType == Integer.class) {
return "INT";
} else if (javaType == long.class || javaType == Long.class) {
return "BIGINT";
} else if (javaType == float.class || javaType == Float.class) {
return "FLOAT";
} else if (javaType == double.class || javaType == Double.class) {
return "DOUBLE";
} else if (javaType == boolean.class || javaType == Boolean.class) {
return "TINYINT(1)";
} else if (javaType == String.class) {
return "VARCHAR(500)";
} else if (javaType == java.util.Date.class || javaType == java.sql.Date.class) {
return "DATE";
} else if (javaType == java.sql.Timestamp.class || javaType == java.time.LocalDateTime.class) {
return "DATETIME";
} else if (javaType == byte[].class) {
return "BLOB";
} else {
return "VARCHAR(1000)";
}
}
private static String extractDbNameFromMysqlUrl(String url) {
if (url == null) return null;
String urlWithoutPrefix = url.replace("jdbc:mysql://", "");
String urlWithoutParams = urlWithoutPrefix.split("\\?")[0];
return urlWithoutParams.substring(urlWithoutParams.lastIndexOf("/") + 1);
}
private static String getConfigValue(Map<String, Object> configMap, String key) {
Object value = configMap.get(key);
return value == null ? null : value.toString().trim();
}
private static boolean isInitialized(DatabaseType dbType) {
return dbType == DatabaseType.SQLITE ? isSqliteInitialized : isMysqlInitialized;
}
private static void markInitialized(DatabaseType dbType, boolean status) {
if (dbType == DatabaseType.SQLITE) {
isSqliteInitialized = status;
} else {
isMysqlInitialized = status;
}
}
private static String getInitSuccessMsg(DatabaseType dbType) {
return dbType == DatabaseType.SQLITE ?
"文件路径: " + sqliteDbFilePath :
"数据库: " + extractDbNameFromMysqlUrl(mysqlUrl) + "、用户: " + mysqlUsername;
}
public static String getActiveDataSource() throws IOException {
// 读取配置文件
try (InputStream yamlInput = new ClassPathResource("application.yml").getInputStream()) {
Yaml yaml = new Yaml();
Map<String, Object> yamlData = yaml.load(yamlInput);
if (yamlData.containsKey("spring")) {
Object springObj = yamlData.get("spring");
if (springObj instanceof Map) {
Map<String, Object> springMap = (Map<String, Object>) springObj;
if (springMap.containsKey("datasource")) {
Object datasourceObj = springMap.get("datasource");
if (datasourceObj instanceof Map) {
Map<String, Object> datasourceMap = (Map<String, Object>) datasourceObj;
if (datasourceMap.containsKey("active")) {
Object activeObj = datasourceMap.get("active");
if (activeObj != null) {
return activeObj.toString().trim();
}
}
}
}
}
}
} catch (Exception e) {
log.error("读取配置文件出错");
}
return "sqlite";
}
}

View File

@ -0,0 +1,18 @@
package com.yj.earth.datasource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
@Configuration
public class JdbcTemplateConfig {
/**
* 配置JdbcTemplate
*/
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}

View File

@ -0,0 +1,35 @@
package com.yj.earth.datasource;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
/**
* MySQL 数据源配置类
*/
@Configuration
@ConditionalOnProperty(name = "spring.datasource.active", havingValue = "mysql")
public class MysqlDataSourceConfig {
/**
* 配置 MySQL 数据源
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.mysql")
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setInitialSize(5);
dataSource.setMaxActive(20);
dataSource.setMinIdle(5);
dataSource.setMaxWait(60000);
dataSource.setTestWhileIdle(true);
dataSource.setValidationQuery("SELECT 1");
dataSource.setTestOnBorrow(false);
dataSource.setTestOnReturn(false);
return dataSource;
}
}

View File

@ -0,0 +1,39 @@
package com.yj.earth.datasource;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
/**
* SQLite 数据源配置类
*/
@Configuration
@ConditionalOnProperty(name = "spring.datasource.active", havingValue = "sqlite", matchIfMissing = true)
public class SqliteDataSourceConfig {
/**
* 配置 SQLite 数据源
*/
@Bean
public DataSource dataSource() {
String dbPath = DatabaseManager.getSqliteDbFilePath();
if (dbPath == null) {
throw new RuntimeException("获取SQLite数据库文件路径失败");
}
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("org.sqlite.JDBC");
dataSource.setUrl("jdbc:sqlite:" + dbPath);
dataSource.setInitialSize(5);
dataSource.setMaxActive(20);
dataSource.setMinIdle(5);
dataSource.setMaxWait(60000);
dataSource.setTestWhileIdle(true);
dataSource.setValidationQuery("SELECT 1");
dataSource.setTestOnBorrow(false);
dataSource.setTestOnReturn(false);
return dataSource;
}
}

View File

@ -0,0 +1,38 @@
package com.yj.earth.design;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class FileInfo {
@Schema(description = "主键")
private String id;
@Schema(description = "文件名")
private String fileName;
@Schema(description = "文件后缀")
private String fileSuffix;
@Schema(description = "内容类型")
private String contentType;
@Schema(description = "文件大小")
private Long fileSize;
@Schema(description = "文件路径")
private String filePath;
@Schema(description = "文件MD5")
private String fileMd5;
@Schema(description = "创建时间")
private LocalDateTime createdAt;
@Schema(description = "更新时间")
private LocalDateTime updatedAt;
}

View File

@ -0,0 +1,28 @@
package com.yj.earth.design;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class Role {
@Schema(description = "主键")
private String id;
@Schema(description = "角色名称")
private String roleName;
@Schema(description = "角色描述")
private String description;
@Schema(description = "是否超级管理员")
private Integer isSuper;
@Schema(description = "创建时间")
private LocalDateTime createdAt;
@Schema(description = "更新时间")
private LocalDateTime updatedAt;
}

View File

@ -0,0 +1,25 @@
package com.yj.earth.design;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class RoleSource {
@Schema(description = "主键")
private String id;
@Schema(description = "角色ID")
private String roleId;
@Schema(description = "资源ID")
private String sourceId;
@Schema(description = "创建时间")
private LocalDateTime createdAt;
@Schema(description = "更新时间")
private LocalDateTime updatedAt;
}

View File

@ -0,0 +1,43 @@
package com.yj.earth.design;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class Source {
@Schema (description = "主键")
private String id;
@Schema (description = "资源名称")
private String sourceName;
@Schema (description = "资源类型")
private String sourceType;
@Schema (description = "资源路径")
private String sourcePath;
@Schema (description = "父级ID")
private String parentId;
@Schema (description = "树状索引")
private Integer treeIndex;
@Schema (description = "是否显示")
private Integer isShow;
@Schema (description = "其他内容")
private String detail;
@Schema (description = "前端参数")
private String params;
@Schema (description = "创建时间")
private LocalDateTime createdAt;
@Schema (description = "更新时间")
private LocalDateTime updatedAt;
}

View File

@ -0,0 +1,38 @@
package com.yj.earth.design;
import com.yj.earth.annotation.ExcludeField;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class User{
@Schema(description = "主键")
private String id;
@Schema(description = "用户名")
private String username;
@Schema(description = "密码")
private String password;
@Schema(description = "头像")
private String avatar;
@Schema(description = "昵称")
private String nickname;
@Schema(description = "手机号")
private String phone;
@Schema(description = "所属角色")
private String roleId;
@Schema(description = "创建时间")
private LocalDateTime createdAt;
@Schema(description = "更新时间")
private LocalDateTime updatedAt;
}

View File

@ -0,0 +1,14 @@
package com.yj.earth.dto.relation;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
public class RoleBindOrUnBindSourceDto {
@Schema(description = "角色ID")
private String roleId;
@Schema(description = "资源ID列表")
private List<String> sourceIdList;
}

View File

@ -0,0 +1,14 @@
package com.yj.earth.dto.relation;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
public class SourceBindOrUnBindRoleDto {
@Schema(description = "角色ID列表")
private List<String> roleIdList;
@Schema(description = "资源ID")
private String sourceId;
}

View File

@ -0,0 +1,14 @@
package com.yj.earth.dto.relation;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
public class UserBindOrUnBindRoleDto {
@Schema(description = "角色ID")
private String roleId;
@Schema(description = "用户ID")
private String userId;
}

View File

@ -0,0 +1,16 @@
package com.yj.earth.dto.role;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class AddRoleDto {
@Schema(description = "角色名称")
private String roleName;
@Schema(description = "角色描述")
private String description;
@Schema(description = "是否超级管理员")
private Integer isSuper;
}

View File

@ -0,0 +1,16 @@
package com.yj.earth.dto.role;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class UpdateRoleDto {
@Schema(description = "主键")
private String id;
@Schema(description = "角色描述")
private String description;
@Schema(description = "是否超级管理员")
private Integer isSuper;
}

View File

@ -0,0 +1,17 @@
package com.yj.earth.dto.source;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class AddDirectoryDto {
@Schema (description = "资源ID")
private String id;
@Schema(description = "资源名称")
private String sourceName;
@Schema (description = "父级ID")
private String parentId;
@Schema (description = "树状索引")
private Integer treeIndex;
}

View File

@ -0,0 +1,18 @@
package com.yj.earth.dto.source;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class AddModelSourceDto {
@Schema (description = "资源ID")
private String id;
@Schema(description = "资源路径")
private String sourcePath;
@Schema(description = "父节点ID")
private String parentId;
@Schema(description = "树状索引")
private Integer treeIndex;
@Schema(description = "前端参数")
private String params;
}

View File

@ -0,0 +1,22 @@
package com.yj.earth.dto.source;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Map;
@Data
public class AddOtherSourceDto {
@Schema (description = "资源ID")
private String id;
@Schema(description = "资源名称")
private String sourceName;
@Schema(description = "资源类型")
private String sourceType;
@Schema(description = "父级ID")
private String parentId;
@Schema(description = "树形索引")
private Integer treeIndex;
@Schema(description = "前端参数")
private Map<String, Object> params;
}

View File

@ -0,0 +1,16 @@
package com.yj.earth.dto.source;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class DragSourceDto {
@Schema(description = "主键")
private String id;
@Schema(description = "父级ID")
private String parentId;
@Schema(description = "树形索引")
private Integer treeIndex;
}

View File

@ -0,0 +1,27 @@
package com.yj.earth.dto.source;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Map;
@Data
public class UpdateSourceDto {
@Schema(description = "主键ID")
private String id;
@Schema(description = "资源名称")
private String sourceName;
@Schema(description = "上级ID")
private String parentId;
@Schema(description = "树形索引")
private Integer treeIndex;
@Schema(description = "是否显示")
private Integer isShow;
@Schema(description = "资源参数")
private Map<String, Object> params;
}

View File

@ -0,0 +1,26 @@
package com.yj.earth.dto.user;
import com.yj.earth.annotation.ExcludeField;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class AddUserDto {
@Schema(description = "用户名")
private String username;
@Schema(description = "密码")
private String password;
@Schema(description = "头像")
private String avatar;
@Schema(description = "昵称")
private String nickname;
@Schema(description = "手机号")
private String phone;
@Schema(description = "所属角色")
private String roleId;
}

View File

@ -0,0 +1,14 @@
package com.yj.earth.dto.user;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class UpdatePasswordDto {
@Schema(description = "用户ID")
private String id;
@Schema(description = "旧密码")
private String oldPassword;
@Schema(description = "新密码")
private String newPassword;
}

View File

@ -0,0 +1,20 @@
package com.yj.earth.dto.user;
import com.yj.earth.annotation.ExcludeField;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class UpdateUserDto {
@Schema(description = "主键")
private String id;
@Schema(description = "头像")
private String avatar;
@Schema(description = "昵称")
private String nickname;
@Schema(description = "手机号")
private String phone;
}

View File

@ -0,0 +1,12 @@
package com.yj.earth.dto.user;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class UserLoginDto {
@Schema(description = "用户名")
private String username;
@Schema(description = "密码")
private String password;
}

View File

@ -0,0 +1,14 @@
package com.yj.earth.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Point {
private double lat; // 纬度
private double lng; // 经度
}

View File

@ -0,0 +1,22 @@
package com.yj.earth.model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
public class RouteRequest {
@Schema(description = "起点纬度")
private Double startLat;
@Schema(description = "起点经度")
private Double startLng;
@Schema(description = "终点纬度")
private Double endLat;
@Schema(description = "终点经度")
private Double endLng;
@Schema(description = "交通方式")
private String profile;
@Schema(description = "途经点")
private List<Point> waypoints;
}

View File

@ -0,0 +1,14 @@
package com.yj.earth.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.List;
@Data
@AllArgsConstructor
public class RouteResponse {
private Double distanceKm; // 距离(公里)
private Double timeMinutes; // 时间(分钟)
private List<Point> pathPoints; // 路径点列表
}

View File

@ -0,0 +1,12 @@
package com.yj.earth.model;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class StatusResponse {
private boolean loading; // 是否正在加载
private boolean loaded; // 是否已加载完成
private String message; // 状态消息
}

View File

@ -0,0 +1,222 @@
package com.yj.earth.params;
import com.yj.earth.annotation.SourceType;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
@Schema(description = "点标注对象")
@SourceType("point")
public class BillboardObject {
@Schema(description = "唯一标识")
private String id;
@Schema(description = "标注整体的显隐", defaultValue = "true")
private boolean show = true;
@Schema(description = "名称")
private String name;
@Schema(description = "位置(必填)")
private Position position = new Position();
@Schema(description = "高度模式0海拔高度1相对地表2依附地表; 3依附模型", defaultValue = "3")
private int heightMode = 3;
@Schema(description = "是否开启跟随视野缩放", defaultValue = "true")
private boolean scaleByDistance = true;
@Schema(description = "视野缩放最近距离", defaultValue = "2000")
private int near = 2000;
@Schema(description = "视野缩放最远距离", defaultValue = "100000")
private int far = 100000;
@Schema(description = "图标参数")
private Billboard billboard = new Billboard();
@Schema(description = "文字参数")
private Label label = new Label();
@Schema(description = "属性内容")
private Attribute attribute = new Attribute();
@Schema(description = "富文本内容")
private String richTextContent;
@Schema(description = "默认视角")
private CustomView customView = new CustomView();
@Data
@Schema(description = "位置属性")
public static class Position {
@Schema(description = "经度")
private double lng;
@Schema(description = "纬度")
private double lat;
@Schema(description = "高度")
private double alt;
}
@Data
@Schema(description = "图标参数")
public static class Billboard {
@Schema(description = "图标显隐", defaultValue = "true")
private boolean show = true;
@Schema(description = "图标路径")
private String image;
@Schema(description = "默认图标的唯一标识")
private String defaultImage;
@Schema(description = "图标放大倍数", defaultValue = "3")
private int scale = 3;
}
@Data
@Schema(description = "文字参数")
public static class Label {
@Schema(description = "文字内容")
private String text;
@Schema(description = "文字显隐", defaultValue = "true")
private boolean show = true;
@Schema(description = "文字字体项0黑体1思源黑体2庞门正道标题体3数黑体", defaultValue = "0")
private int fontFamily = 0;
@Schema(description = "文字大小、单位px", defaultValue = "39")
private int fontSize = 39;
@Schema(description = "文字颜色", defaultValue = "#00ffff")
private String color = "#00ffff";
}
@Data
@Schema(description = "属性内容")
public static class Attribute {
@Schema(description = "链接")
private Link link = new Link();
@Schema(description = "全景图")
private Vr vr = new Vr();
@Schema(description = "摄像头")
private Camera camera = new Camera();
@Schema(description = "ISC")
private Isc isc = new Isc();
@Schema(description = "物资")
private Goods goods = new Goods();
@Data
@Schema(description = "链接属性")
public static class Link {
@Schema(description = "链接内容列表")
private List<LinkContent> content = new ArrayList<>();
@Data
@Schema(description = "链接内容")
public static class LinkContent {
@Schema(description = "链接名称")
private String name;
@Schema(description = "链接地址")
private String url;
}
}
@Data
@Schema(description = "全景图属性")
public static class Vr {
@Schema(description = "全景图内容列表")
private List<VrContent> content = new ArrayList<>();
@Data
@Schema(description = "全景图内容")
public static class VrContent {
@Schema(description = "名称")
private String name;
@Schema(description = "地址")
private String url;
}
}
@Data
@Schema(description = "摄像头属性")
public static class Camera {
@Schema(description = "摄像头内容列表")
private List<Object> content = new ArrayList<>();
}
@Data
@Schema(description = "ISC属性")
public static class Isc {
@Schema(description = "ISC内容列表")
private List<Object> content = new ArrayList<>();
}
@Data
@Schema(description = "物资属性")
public static class Goods {
@Schema(description = "物资内容列表")
private List<GoodsContent> content = new ArrayList<>();
@Data
@Schema(description = "物资内容")
public static class GoodsContent {
@Schema(description = "id")
private String id;
@Schema(description = "名称")
private String name;
@Schema(description = "数量")
private String cnt;
}
}
}
@Data
@Schema(description = "默认视角属性")
public static class CustomView {
@Schema(description = "默认视角方位")
private Orientation orientation = new Orientation();
@Schema(description = "视角相对位置")
private RelativePosition relativePosition = new RelativePosition();
@Data
@Schema(description = "视角方位属性")
public static class Orientation {
@Schema(description = "航向角")
private double heading;
@Schema(description = "俯仰角")
private double pitch;
@Schema(description = "翻滚角")
private double roll;
}
@Data
@Schema(description = "视角相对位置属性")
public static class RelativePosition {
@Schema(description = "经度")
private double lng;
@Schema(description = "纬度")
private double lat;
@Schema(description = "高度")
private double alt;
}
}
}

View File

@ -0,0 +1,70 @@
package com.yj.earth.params;
import com.yj.earth.annotation.SourceType;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
@SourceType("circle")
public class Circle {
private String id;
private String name;
private Center center;
private int radius;
private Map<String, Object> customView;
private boolean show;
private String color;
private int heightMode;
private Line line;
private Label label;
private Attribute attribute;
private String richTextContent;
@Data
public static class Center {
private double lng;
private double lat;
private double alt;
}
@Data
public static class Line {
private int width;
private String color;
}
@Data
public static class Label {
private String text;
private boolean show;
private Position position;
private int fontSize;
private int fontFamily;
private String color;
private int lineWidth;
private int pixelOffset;
private List<String> backgroundColor;
private String lineColor;
private boolean scaleByDistance;
private int near;
private int far;
@Data
public static class Position {
private double lng;
private double lat;
private double alt;
}
}
@Data
public static class Attribute {
private Link link;
@Data
public static class Link {
private List<Object> content;
}
}
}

View File

@ -0,0 +1,192 @@
package com.yj.earth.params;
import com.yj.earth.annotation.SourceType;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
@Schema(description = "曲线对象")
@SourceType("curve")
public class CurvelineObject {
@Schema(description = "唯一标识")
private String id;
@Schema(description = "名称")
private String name;
@Schema(description = "首尾相反", defaultValue = "false")
private boolean rotate = false;
@Schema(description = "间距", defaultValue = "1")
private int space = 1;
@Schema(description = "速度", defaultValue = "10")
private String speed = "10";
@Schema(description = "空间单位名称", defaultValue = "0")
private String wordsName;
@Schema(description = "长度单位", defaultValue = "0")
private String lengthUnit;
@Schema(description = "线宽", defaultValue = "3")
private double width = 3;
@Schema(description = "颜色", defaultValue = "#ff0000")
private String color = "#ff0000";
@Schema(description = "材质类型 0-实线 1-虚线 2-泛光...", defaultValue = "0")
private int type = 0;
@Schema(description = "高度模式0海拔高度1相对高度2依附模式", defaultValue = "2")
private int heightMode = 2;
@Schema(description = "首尾相连", defaultValue = "false")
private boolean noseToTail = false;
@Schema(description = "线缓冲", defaultValue = "false")
private boolean extend = false;
@Schema(description = "线缓冲宽度", defaultValue = "10")
private double extendWidth = 10;
@Schema(description = "线缓冲颜色", defaultValue = "rgba(255,255,80,0.3)")
private String extendColor = "rgba(255,255,80,0.3)";
@Schema(description = "显隐", defaultValue = "true")
private boolean show = true;
@Schema(description = "经纬度和高度的列表(必填)")
private List<Position> positions = new ArrayList<>();
@Schema(description = "标签对象")
private Label label = new Label();
@Schema(description = "属性内容")
private Attribute attribute = new Attribute();
@Schema(description = "富文本内容")
private String richTextContent;
@Schema(description = "默认视角")
private CustomView customView = new CustomView();
@Data
@Schema(description = "位置属性")
public static class Position {
@Schema(description = "经度")
private double lng;
@Schema(description = "纬度")
private double lat;
@Schema(description = "高度")
private double alt;
}
@Data
@Schema(description = "标签参数")
public static class Label {
@Schema(description = "标签文本")
private String text;
@Schema(description = "标签显隐")
private Boolean show;
@Schema(description = "标签位置")
private Position position = new Position();
@Schema(description = "字体大小", defaultValue = "20")
private int fontSize = 20;
@Schema(description = "字体项 0黑体1思源黑体2庞门正道标题体3数黑体", defaultValue = "0")
private int fontFamily = 0;
@Schema(description = "字体颜色", defaultValue = "#ffffff")
private String color = "#ffffff";
@Schema(description = "引线宽", defaultValue = "4")
private double lineWidth = 4;
@Schema(description = "引线颜色", defaultValue = "#00ffff80")
private String lineColor = "#00ffff80";
@Schema(description = "字体偏移(引线长度)", defaultValue = "20")
private double pixelOffset = 20;
@Schema(description = "背景颜色", defaultValue = "['#00ffff80', '#00ffff80']")
private String[] backgroundColor = {"#00ffff80", "#00ffff80"};
@Schema(description = "距离缩放")
private Boolean scaleByDistance;
@Schema(description = "视野缩放最近距离", defaultValue = "2000")
private int near = 2000;
@Schema(description = "视野缩放最远距离", defaultValue = "100000")
private int far = 100000;
}
@Data
@Schema(description = "属性内容")
public static class Attribute {
@Schema(description = "链接")
private Link link = new Link();
@Data
@Schema(description = "链接属性")
public static class Link {
@Schema(description = "链接内容列表", defaultValue = "[]")
private List<LinkContent> content = new ArrayList<>();
@Data
@Schema(description = "链接内容")
public static class LinkContent {
@Schema(description = "链接名称")
private String name;
@Schema(description = "链接地址")
private String url;
}
}
}
@Data
@Schema(description = "默认视角属性")
public static class CustomView {
@Schema(description = "默认视角方位")
private Orientation orientation = new Orientation();
@Schema(description = "视角相对位置")
private RelativePosition relativePosition = new RelativePosition();
@Data
@Schema(description = "视角方位属性")
public static class Orientation {
@Schema(description = "航向角")
private double heading;
@Schema(description = "俯仰角")
private double pitch;
@Schema(description = "翻滚角")
private double roll;
}
@Data
@Schema(description = "视角相对位置属性")
public static class RelativePosition {
@Schema(description = "经度")
private double lng;
@Schema(description = "纬度")
private double lat;
@Schema(description = "高度")
private double alt;
}
}
}

Some files were not shown because too many files have changed in this diff Show More