1.优化地区csv加载机制 2.修复数据库监控无法访问问题 3.增加了登录状态保存 4.针对session存储机制做适配 5.文件扫描机制优化,并行扫描速度更快

This commit is contained in:
2026-06-30 18:21:30 +08:00
parent 694cd8d3ee
commit 3c0aa59691
17 changed files with 175 additions and 133 deletions
@@ -22,6 +22,8 @@ import java.nio.channels.FileChannel;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static java.nio.file.StandardWatchEventKinds.*;
@@ -50,6 +52,9 @@ public class FileWatchServiceManager {
private String rootReceiveDir;
private volatile boolean running = false;
/** 文件处理线程池,用于并发 MD5 计算 + 入库 */
private final ExecutorService fileProcessExecutor = Executors.newFixedThreadPool(4);
@PostConstruct
public void init() {
try {
@@ -72,6 +77,7 @@ public class FileWatchServiceManager {
log.warn("WatchService 关闭异常", e);
}
}
fileProcessExecutor.shutdown();
}
/** 确保文件扫描目录结构存在(receive/display/archive 及各部门子目录) */
@@ -188,7 +194,8 @@ public class FileWatchServiceManager {
/** 扫描部门目录下的所有文件 */
private void scanDirectory(Path deptDir, Long deptId) {
try (var fileStream = Files.list(deptDir)) {
fileStream.filter(Files::isRegularFile).forEach(file -> processFile(file, deptId));
fileStream.filter(Files::isRegularFile).forEach(file ->
fileProcessExecutor.submit(() -> processFile(file, deptId)));
} catch (IOException e) {
log.error("扫描目录失败: {}", deptDir, e);
}
@@ -212,7 +219,7 @@ public class FileWatchServiceManager {
Long deptId = deptDirMap.get(watchDir.toString());
// deptDirMap 中没有的是根目录,deptId 为 null
processFile(fullPath, deptId);
fileProcessExecutor.submit(() -> processFile(fullPath, deptId));
}
if (!key.reset()) {
@@ -227,8 +234,24 @@ public class FileWatchServiceManager {
}
}
/** 等待文件写入完成并可读 */
/**
* 等待文件写入完成并可读。
* 先快速路径尝试获取共享锁(已有文件通常立即可用),
* 若被占用再进入大小稳定 + 等待循环。
*/
private boolean waitForFileReady(Path filePath) {
if (!Files.exists(filePath)) return false;
// 快速路径:已写完的文件可立即获得共享锁,直接返回
try (RandomAccessFile raf = new RandomAccessFile(filePath.toFile(), "rw");
FileChannel ch = raf.getChannel()) {
java.nio.channels.FileLock lock = ch.lock(0, Long.MAX_VALUE, true);
lock.release();
return true;
} catch (Exception ignored) {
// 文件正在被写入,进入等待逻辑
}
long lastSize = -1;
int stableCount = 0;
int maxRetries = 20;
@@ -39,7 +39,7 @@ import java.util.Map;
*/
@RestController
@RequestMapping("station/weatherstation")
@Tag(name = "站点")
@Tag(name = "气象站点")
public class WeatherStationController {
@Resource
@@ -33,19 +33,11 @@ spring:
test-on-borrow: false
test-on-return: false
stat-view-servlet:
allow: localhost
enabled: true
url-pattern: /druid/*
#login-username: admin
#login-password: admin
#达梦数据库,需要注释掉,其他数据库可以打开
# filter:
# stat:
# log-slow-sql: true
# slow-sql-millis: 1000
# merge-sql: false
# wall:
# config:
# multi-statement-allow: true
# login-username: admin
# login-password: admin
# 是否开启redis缓存 true开启 false关闭
project-options:
@@ -33,6 +33,7 @@ spring:
test-on-borrow: false
test-on-return: false
stat-view-servlet:
allow: localhost
enabled: true
url-pattern: /druid/*
#login-username: admin
@@ -33,10 +33,11 @@ spring:
test-on-borrow: false
test-on-return: false
stat-view-servlet:
allow: 0.0.0.0
enabled: true
url-pattern: /druid/*
#login-username: admin
#login-password: admin
# login-username: admin
# login-password: admin
filter:
stat:
log-slow-sql: true
+1 -1
View File
@@ -1,3 +1,3 @@
NODE_ENV=development
VITE_APP_API=http://192.168.2.151:48080/system-admin
VITE_APP_API=/system-admin
@@ -0,0 +1,59 @@
/**
* 地区数据模块级单例缓存。
* 首次调用时 fetch /region.csv 并构建 idMap 和 parentMap
* 后续调用直接返回缓存,SPA 生命周期内只加载一次。
*/
interface RegionItem {
id: number;
parentId: number;
level: number;
name: string;
}
interface RegionCache {
/** id → RegionItem,用于表格列的 id→name 转换 */
idMap: Map<number, RegionItem>;
/** parentId → children[],用于省/市/区下拉联动 O(1) 查找 */
parentMap: Map<number, RegionItem[]>;
/** 所有地区平铺数组 */
list: RegionItem[];
}
let _cache: RegionCache | null = null;
function parseCSV(text: string): RegionItem[] {
const lines = text.split("\n").filter((l) => l.trim());
const headers = lines[0].split(",").map((h) => h.trim());
return lines.slice(1).map((line) => {
const cols = line.split(",").map((c) => c.trim());
const obj: any = {};
headers.forEach((h, i) => {
obj[h] = h === "name" ? cols[i] : Number(cols[i]);
});
return obj as RegionItem;
});
}
export async function useRegionCache(): Promise<RegionCache> {
if (_cache) return _cache;
const res = await fetch("/region.csv");
if (!res.ok) throw new Error("地区数据加载失败");
const text = await res.text();
const list = parseCSV(text);
const idMap = new Map<number, RegionItem>();
const parentMap = new Map<number, RegionItem[]>();
for (const item of list) {
idMap.set(item.id, item);
if (!parentMap.has(item.parentId)) {
parentMap.set(item.parentId, []);
}
parentMap.get(item.parentId)!.push(item);
}
_cache = { idMap, parentMap, list };
return _cache;
}
@@ -91,6 +91,14 @@ export function useWeatherChart(
async function renderChart() {
if (!chartRef.value) return;
// 确保 DOM 已渲染且有尺寸,避免 ECharts "invalid dom" 报错
const { width, height } = chartRef.value.getBoundingClientRect();
if (width === 0 || height === 0) {
// DOM 未就绪,延迟重试
scheduleRender();
return;
}
// 动态按需加载 ECharts
const echarts = await import("echarts");
const mod = (echarts as any).default || echarts;
+1 -1
View File
@@ -67,7 +67,7 @@ export const useAppStore = defineStore("useAppStore", {
},
//退出
logout() {
removeCache(CacheToken, true);
removeCache(CacheToken);
this.updateState({
appIsLogin: false,
permissions: [],
+1 -1
View File
@@ -67,5 +67,5 @@ export const removeCache = (key: string, isSessionStorage?: boolean): void => {
};
export const getToken = (): string => {
return getCache(CacheToken, { isSessionStorage: true }, {})["token"];
return getCache(CacheToken, {}, {})["token"];
};
+1 -1
View File
@@ -98,7 +98,7 @@ const onLogin = () => {
.then((res) => {
state.loading = false;
if (res.code === 0) {
setCache(CacheToken, res.data, true);
setCache(CacheToken, res.data);
ElMessage.success("登录成功");
router.push("/");
} else {
+14 -42
View File
@@ -28,6 +28,7 @@
<script lang="ts" setup>
import { ref, onMounted, computed } from 'vue';
import { useRegionCache } from '@/composables/useRegionCache';
const rawList = ref<any[]>([]);
const treeData = ref<any[]>([]);
@@ -35,61 +36,32 @@ const loadingStatus = ref('正在解析数据...');
const rawListCount = computed(() => rawList.value.length);
// 优化后的构建树函数
const buildTreeMap = (list: any[]) => {
const map = new Map<number, any[]>();
// 构建 parentId -> children 的 Map
list.forEach(item => {
const pid = item.parentid;
if (!map.has(pid)) map.set(pid, []);
map.get(pid)!.push({ ...item });
// 从 parentMap 递归构建树
const buildTreeFromCache = (parentMap: Map<number, any[]>, pId: number): any[] => {
const children = parentMap.get(pId) || [];
return children.map((child) => {
const node = { ...child };
const sub = buildTreeFromCache(parentMap, child.id);
if (sub.length > 0) node.children = sub;
return node;
});
// 递归构建树
const createTree = (pId: number): any[] => {
const children = map.get(pId) || [];
children.forEach(child => {
const sub = createTree(child.id);
if (sub.length > 0) child.children = sub;
});
return children;
};
// 自动探测根节点
const rootParentId = Math.min(...list.map(r => r.parentid));
return createTree(rootParentId);
};
const initRegionTree = async () => {
try {
const res = await fetch("/region.csv");
if (!res.ok) throw new Error("无法读取数据,请检查文件");
const cache = await useRegionCache();
rawList.value = cache.list;
const text = await res.text();
const lines = text.split("\n").map(l => l.trim()).filter(l => l);
if (lines.length < 2) throw new Error("内容为空或格式错误");
// 自动探测根节点(最小 parentId)
const rootParentId = Math.min(...cache.list.map((r) => r.parentId));
const tree = buildTreeFromCache(cache.parentMap, rootParentId);
const headers = lines[0].split(",").map(h => h.trim().toLowerCase());
rawList.value = lines.slice(1).map(line => {
const cols = line.split(",").map(c => c.trim());
const obj: any = {};
headers.forEach((h, i) => {
obj[h] = h === 'name' ? cols[i] : Number(cols[i]);
});
return obj;
});
// 构建树
const tree = buildTreeMap(rawList.value);
if (tree.length === 0) {
loadingStatus.value = '构建失败:根节点下无数据';
} else {
treeData.value = tree;
loadingStatus.value = '加载完成';
}
} catch (error: any) {
loadingStatus.value = `错误: ${error.message}`;
console.error(error);
@@ -21,12 +21,7 @@
<el-col :span="12">
<el-form-item label="测站级别" prop="stationLevel">
<el-select v-model="dataForm.stationLevel" placeholder="选择级别">
<el-option label="国家基准气候站(11" :value="11"/>
<el-option label="国家基本气象站(12" :value="12"/>
<el-option label="国家一般气象站(13" :value="13"/>
<el-option label="区域气象站(14" :value="14"/>
<el-option label="其他气象站(15" :value="15"/>
<el-option label="国家站(16" :value="16"/>
<el-option v-for="item in stationLevelOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</el-col>
@@ -135,11 +130,15 @@
import { reactive, ref, computed, nextTick } from "vue";
import baseService from "@/service/baseService";
import { ElMessage } from "element-plus";
import { useAppStore } from "@/store";
import { getDictDataList } from "@/utils/utils";
// 接收父页面传来的 CSV 原始数组和 Map 索引
const store = useAppStore();
// 接收父页面传来的地区数据
const props = defineProps<{
regionsData: any[]
regionMap: Map<number, any>
parentMap: Map<number, any[]>
deptData: any[]
}>();
const emit = defineEmits(["refreshDataList"]);
@@ -170,14 +169,19 @@ const rules = {
stationLevel: [{ required: true, message: "请选择测站级别", trigger: "change" }]
};
// ----------------- 下拉框联动逻辑 -----------------
// ----------------- 字典数据 ----------------
// 1. 获取省级列表 (根据你说的:parentId 为 1 是省级)
const provinceOptions = computed(() => {
const list = props.regionsData.filter(r => Number(r.parentId) === 1);
console.log("省级加载检查,匹配数量:", list.length);
return list;
});
const stationLevelOptions = computed(() =>
getDictDataList(store.state.dicts, "station_level").map((item: any) => ({
label: `${item.dictLabel}${item.dictValue}`,
value: Number(item.dictValue)
}))
);
// ----------------- 下拉框联动逻辑(O(1) 查找) -----------------
// 省级:parentId = 1
const provinceOptions = computed(() => props.parentMap.get(1) || []);
// 市、区列表:动态变化
const cityOptions = ref<any[]>([]);
@@ -189,9 +193,8 @@ const onProvinceChange = (val: string) => {
dataForm.countyName = "";
cityOptions.value = [];
countyOptions.value = [];
if (val) {
cityOptions.value = props.regionsData.filter(r => Number(r.parentId) === Number(val));
cityOptions.value = props.parentMap.get(Number(val)) || [];
}
};
@@ -199,18 +202,11 @@ const onProvinceChange = (val: string) => {
const onCityChange = (val: string) => {
dataForm.countyName = "";
countyOptions.value = [];
if (val) {
countyOptions.value = props.regionsData.filter(r => Number(r.parentId) === Number(val));
countyOptions.value = props.parentMap.get(Number(val)) || [];
}
};
function getDeptList() {
return baseService.get("/sys/dept/list").then((res) => {
deptList.value = res.data || [];
});
}
function onDeptClick(data: any) {
dataForm.deptId = data.id;
}
@@ -220,9 +216,11 @@ function onDeptClick(data: any) {
const init = (id?: number) => {
visible.value = true;
// 重置表单和选项
// 重置选项
cityOptions.value = [];
countyOptions.value = [];
// 复用父页面的部门数据,无需重复请求
deptList.value = props.deptData || [];
nextTick(() => {
if (dataFormRef.value) dataFormRef.value.resetFields();
@@ -234,13 +232,10 @@ const init = (id?: number) => {
townName: "",
deptId: ""
});
getDeptList().then(() => {
if (id) {
getInfo(id);
}
});
});
};
// 编辑回显
@@ -251,12 +246,12 @@ const getInfo = async (id: number) => {
// 1. 基础数据赋值
Object.assign(dataForm, data);
// 2. 关键:手动触发联动列表加载,否则 Select 只有 ID 没有数据源,无法显示名字
// 2. 手动加载省市区联动列表数据源
if (data.provinceName) {
cityOptions.value = props.regionsData.filter(r => Number(r.parentId) === Number(data.provinceName));
cityOptions.value = props.parentMap.get(Number(data.provinceName)) || [];
}
if (data.cityName) {
countyOptions.value = props.regionsData.filter(r => Number(r.parentId) === Number(data.cityName));
countyOptions.value = props.parentMap.get(Number(data.cityName)) || [];
}
// 3. 回显选择的管理部门
@@ -59,7 +59,7 @@
<el-pagination :current-page="state.page" :page-sizes="[10, 20, 50, 100]" :page-size="state.limit" :total="state.total" layout="total, sizes, prev, pager, next, jumper" @size-change="state.pageSizeChangeHandle" @current-change="state.pageCurrentChangeHandle"> </el-pagination>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update ref="addOrUpdateRef" :regions-data="regionsData" :region-map="regionMap" @refreshDataList="state.getDataList"> </add-or-update>
<add-or-update ref="addOrUpdateRef" :parent-map="parentMap" :dept-data="deptData" @refreshDataList="state.getDataList"> </add-or-update>
</div>
</template>
@@ -68,6 +68,7 @@ import useView from "@/hooks/useView";
import { reactive, ref, toRefs, onMounted } from "vue";
import AddOrUpdate from "./weatherstation-add-or-update.vue";
import baseService from "@/service/baseService";
import { useRegionCache } from "@/composables/useRegionCache";
// 表格 state
const view = reactive({
@@ -97,22 +98,25 @@ const addOrUpdateHandle = (id?: number) => {
//
// }
// ---------------- CSV 数据加载 ----------------
const regionsData = ref<any[]>([]);
// ---------------- 地区数据(模块级缓存) ----------------
const parentMap = reactive(new Map<number, any[]>());
const regionMap = reactive(new Map<number, any>());
// ---------------- 部门数据加载 ----------------
// ---------------- 部门数据 ----------------
const deptMap = reactive(new Map<number, string>());
const deptData = ref<any[]>([]);
async function loadDepts() {
try {
const res = await baseService.get("/sys/dept/list");
deptData.value = res.data || [];
const flat = (list: any[]) => {
list.forEach((d) => {
list.forEach((d: any) => {
deptMap.set(Number(d.id), d.name);
if (d.children) flat(d.children);
});
};
flat(res.data || []);
flat(deptData.value);
} catch {
// ignore
}
@@ -120,26 +124,8 @@ async function loadDepts() {
onMounted(async () => {
loadDepts();
const res = await fetch("/region.csv");
const text = await res.text();
const parseCSV = (csvText: string) => {
const lines = csvText.split("\n").filter((l) => l.trim());
const headers = lines[0].split(",");
return lines.slice(1).map((line) => {
const cols = line.split(",");
const obj: any = {};
headers.forEach((h, i) => (obj[h.trim()] = cols[i].trim()));
return obj;
});
};
const data = parseCSV(text);
data.forEach((r) => {
const id = Number(r.id);
const parentId = Number(r.parentId);
const level = Number(r.level);
const name = r.name;
regionMap.set(id, { id, parentId, level, name });
regionsData.value.push({ id, parentId, level, name });
});
const cache = await useRegionCache();
cache.idMap.forEach((item, id) => regionMap.set(id, item));
cache.parentMap.forEach((children, pid) => parentMap.set(pid, children));
});
</script>
@@ -74,9 +74,8 @@
<script lang="ts" setup>
import app from "@/constants/app";
import { CacheToken } from "@/constants/cacheKey";
import { getToken } from "@/utils/cache";
import baseService from "@/service/baseService";
import { getCache } from "@/utils/cache";
import { ElMessage } from "element-plus";
import axios from "axios";
import { computed, onMounted, onUnmounted, reactive, ref } from "vue";
@@ -134,7 +133,7 @@ const loadBlobUrl = (fileId: string): Promise<string> => {
const base = (app.api || "").replace(/\/$/, "");
const url = `${base}/filescan/file/display/${fileId}`;
const token = getCache(CacheToken, { isSessionStorage: true }, {})?.token;
const token = getToken();
const headers: Record<string, string> = {};
if (token) headers.token = token;
@@ -75,9 +75,9 @@
<script lang="ts" setup>
import app from "@/constants/app";
import { CacheToken } from "@/constants/cacheKey";
import baseService from "@/service/baseService";
import { getCache } from "@/utils/cache";
import { getToken } from "@/utils/cache";
import { copyToClipboard } from "@/utils/utils";
import { ElMessage } from "element-plus";
import axios from "axios";
@@ -137,7 +137,7 @@ const loadBlobUrl = (fileId: string): Promise<string> => {
const base = (app.api || "").replace(/\/$/, "");
const url = `${base}/filescan/file/display/${fileId}`;
const token = getCache(CacheToken, { isSessionStorage: true }, {})?.token;
const token = getToken();
const headers: Record<string, string> = {};
if (token) headers.token = token;
+7 -1
View File
@@ -59,7 +59,13 @@ export default (config: UserConfig): UserConfigExport => {
open: false, // 自动启动浏览器
host: "0.0.0.0", // localhost
port: 8001, // 端口号
hmr: { overlay: false }
hmr: { overlay: false },
proxy: {
"/system-admin": {
target: "http://192.168.2.151:48080",
changeOrigin: true
}
}
}
});
};