feat(database): 实现数据库功能并优化数据导出
- 新增数据库相关 API 和服务 - 实现数据导出功能,支持导出到 JSON 文件 - 优化数据导入流程,增加数据校验 - 新增数据库页面,展示解析数据和统计信息 - 更新捕获页面,支持导入数据到数据库
This commit is contained in:
95
README.md
95
README.md
@@ -7,7 +7,11 @@
|
|||||||
- 🎯 **TCP包抓取** - 实时抓取游戏客户端的TCP通信数据
|
- 🎯 **TCP包抓取** - 实时抓取游戏客户端的TCP通信数据
|
||||||
- 🔍 **数据解析** - 自动解析十六进制数据为可读的装备信息
|
- 🔍 **数据解析** - 自动解析十六进制数据为可读的装备信息
|
||||||
- 📊 **数据可视化** - 现代化的React界面展示装备数据
|
- 📊 **数据可视化** - 现代化的React界面展示装备数据
|
||||||
- 💾 **数据导出** - 支持导出为JSON格式
|
- 💾 **数据持久化** - 基于SQLite的本地数据存储,支持装备数据的增删改查
|
||||||
|
- 📤 **数据导出** - 支持导出为JSON格式
|
||||||
|
- 🔍 **数据搜索** - 支持装备数据的模糊搜索和筛选
|
||||||
|
- 📈 **数据统计** - 提供装备数据的统计分析功能
|
||||||
|
- ⚙️ **设置管理** - 应用设置的本地持久化存储
|
||||||
- 🚀 **高性能** - 基于Go语言的高性能网络处理
|
- 🚀 **高性能** - 基于Go语言的高性能网络处理
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
@@ -17,6 +21,7 @@
|
|||||||
- **Wails v2** - 桌面应用框架
|
- **Wails v2** - 桌面应用框架
|
||||||
- **gopacket** - 网络包抓取
|
- **gopacket** - 网络包抓取
|
||||||
- **zap** - 结构化日志
|
- **zap** - 结构化日志
|
||||||
|
- **SQLite** - 本地数据存储
|
||||||
|
|
||||||
### 前端
|
### 前端
|
||||||
- **React 18** - 用户界面框架
|
- **React 18** - 用户界面框架
|
||||||
@@ -109,6 +114,56 @@ equipment-analyzer/
|
|||||||
|
|
||||||
## 开发指南
|
## 开发指南
|
||||||
|
|
||||||
|
### 常见问题解决
|
||||||
|
|
||||||
|
#### 1. React严格模式导致的重复调用
|
||||||
|
在开发模式下,React严格模式会导致组件渲染两次,这可能导致:
|
||||||
|
- useEffect被重复执行
|
||||||
|
- 异步操作被重复调用
|
||||||
|
- 用户提示消息重复显示
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
- **全局消息去重配置**:在`frontend/src/utils/messageConfig.ts`中配置Antd message的全局设置
|
||||||
|
- **防重复机制**:使用缓存机制防止2秒内相同消息重复显示
|
||||||
|
- **最大显示数量限制**:设置`maxCount: 1`确保同时只显示一个消息
|
||||||
|
|
||||||
|
**全局配置:**
|
||||||
|
```typescript
|
||||||
|
// frontend/src/utils/messageConfig.ts
|
||||||
|
import { message } from 'antd';
|
||||||
|
|
||||||
|
// 配置全局消息设置,防止重复显示
|
||||||
|
message.config({
|
||||||
|
maxCount: 1, // 最多同时显示1个消息
|
||||||
|
duration: 3, // 消息显示3秒
|
||||||
|
top: 24, // 距离顶部24px
|
||||||
|
});
|
||||||
|
|
||||||
|
// 防重复消息函数
|
||||||
|
const messageCache = new Map<string, number>();
|
||||||
|
const DEBOUNCE_TIME = 2000; // 2秒内相同消息不重复显示
|
||||||
|
|
||||||
|
export const showMessage = (type: 'success' | 'error' | 'warning' | 'info', content: string) => {
|
||||||
|
const messageId = `${type}:${content}`;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// 检查是否在防抖时间内
|
||||||
|
const lastTime = messageCache.get(messageId);
|
||||||
|
if (lastTime && now - lastTime < DEBOUNCE_TIME) {
|
||||||
|
return; // 跳过重复消息
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新缓存并显示消息
|
||||||
|
messageCache.set(messageId, now);
|
||||||
|
message[type](content);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用方法:**
|
||||||
|
- 在`main.tsx`中导入配置:`import './utils/messageConfig'`
|
||||||
|
- 在组件中正常使用`message.success()`、`message.error()`等
|
||||||
|
- 全局配置会自动防止重复消息显示
|
||||||
|
|
||||||
### 添加新功能
|
### 添加新功能
|
||||||
|
|
||||||
1. **后端功能**
|
1. **后端功能**
|
||||||
@@ -121,6 +176,23 @@ equipment-analyzer/
|
|||||||
- 使用TypeScript确保类型安全
|
- 使用TypeScript确保类型安全
|
||||||
- 遵循Ant Design设计规范
|
- 遵循Ant Design设计规范
|
||||||
|
|
||||||
|
### 数据库操作
|
||||||
|
|
||||||
|
1. **添加新的数据表**
|
||||||
|
- 在`internal/model/database.go`的`initTables()`方法中添加表结构
|
||||||
|
- 在`Database`结构体中添加相应的CRUD方法
|
||||||
|
- 在`DatabaseService`中添加业务逻辑方法
|
||||||
|
- 在`App`中添加API接口方法
|
||||||
|
|
||||||
|
2. **数据迁移**
|
||||||
|
- 数据库结构变更时,在`initTables()`方法中处理版本升级
|
||||||
|
- 使用事务确保数据一致性
|
||||||
|
|
||||||
|
3. **性能优化**
|
||||||
|
- 为常用查询字段添加索引
|
||||||
|
- 使用批量操作减少数据库交互
|
||||||
|
- 合理使用连接池和事务
|
||||||
|
|
||||||
### 测试
|
### 测试
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -147,8 +219,27 @@ make release
|
|||||||
make clean
|
make clean
|
||||||
```
|
```
|
||||||
|
|
||||||
## 配置说明
|
## 数据存储
|
||||||
|
|
||||||
|
### 数据库存储
|
||||||
|
应用使用SQLite数据库进行本地数据持久化,数据库文件位置:`~/.equipment-analyzer/equipment_analyzer.db`
|
||||||
|
|
||||||
|
#### 数据库表结构
|
||||||
|
- **equipment** - 装备基本信息表
|
||||||
|
- **equipment_operations** - 装备操作属性表
|
||||||
|
- **capture_sessions** - 抓包会话记录表
|
||||||
|
- **raw_packet_data** - 原始数据包表
|
||||||
|
- **app_settings** - 应用设置表
|
||||||
|
|
||||||
|
#### 数据库功能
|
||||||
|
- ✅ 装备数据的增删改查
|
||||||
|
- ✅ 批量数据导入导出
|
||||||
|
- ✅ 装备数据搜索和筛选
|
||||||
|
- ✅ 数据统计分析
|
||||||
|
- ✅ 应用设置持久化
|
||||||
|
- ✅ 抓包会话历史记录
|
||||||
|
|
||||||
|
### 配置文件
|
||||||
应用会在用户目录下创建配置文件:`~/.equipment-analyzer/config.json`
|
应用会在用户目录下创建配置文件:`~/.equipment-analyzer/config.json`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import React, {useEffect, useState, useRef} from 'react'
|
import React from 'react'
|
||||||
import {Button, Card, Layout, message, Select, Spin, Table} from 'antd'
|
import {Layout, Menu} from 'antd'
|
||||||
import {DownloadOutlined, PlayCircleOutlined, SettingOutlined, StopOutlined} from '@ant-design/icons'
|
|
||||||
import './App.css'
|
import './App.css'
|
||||||
import {ExportData, GetCapturedData,GetNetworkInterfaces,
|
import {BrowserRouter as Router, Link, Route, Routes, useLocation} from 'react-router-dom';
|
||||||
ParseData,StartCapture,StopCapture, ReadRawJsonFile
|
|
||||||
} from "../wailsjs/go/service/App";
|
|
||||||
import { BrowserRouter as Router, Routes, Route, Link, useLocation } from 'react-router-dom';
|
|
||||||
import { Menu } from 'antd';
|
|
||||||
import CapturePage from './pages/CapturePage';
|
import CapturePage from './pages/CapturePage';
|
||||||
import OptimizerPage from './pages/OptimizerPage';
|
import OptimizerPage from './pages/OptimizerPage';
|
||||||
|
import DatabasePage from './pages/DatabasePage';
|
||||||
|
|
||||||
const {Header, Content, Sider} = Layout
|
const {Header, Content, Sider} = Layout
|
||||||
|
|
||||||
@@ -51,6 +47,9 @@ function AppContent() {
|
|||||||
<Menu.Item key="/">
|
<Menu.Item key="/">
|
||||||
<Link to="/">抓包</Link>
|
<Link to="/">抓包</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
<Menu.Item key="/database">
|
||||||
|
<Link to="/database">数据库</Link>
|
||||||
|
</Menu.Item>
|
||||||
<Menu.Item key="/optimizer">
|
<Menu.Item key="/optimizer">
|
||||||
<Link to="/optimizer">配装优化</Link>
|
<Link to="/optimizer">配装优化</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
@@ -59,6 +58,7 @@ function AppContent() {
|
|||||||
<Content style={{ padding: 0, minHeight: 280 }}>
|
<Content style={{ padding: 0, minHeight: 280 }}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<CapturePage />} />
|
<Route path="/" element={<CapturePage />} />
|
||||||
|
<Route path="/database" element={<DatabasePage />} />
|
||||||
<Route path="/optimizer" element={<OptimizerPage />} />
|
<Route path="/optimizer" element={<OptimizerPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Content>
|
</Content>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React, {useEffect, useRef, useState} from 'react';
|
import React, {useEffect, useRef, useState} from 'react';
|
||||||
import {Button, Card, Layout, message, Select, Spin, Table} from 'antd';
|
import {Button, Card, Layout, message, Select, Spin, Table} from 'antd';
|
||||||
import {DownloadOutlined, PlayCircleOutlined, SettingOutlined, StopOutlined} from '@ant-design/icons';
|
import {DownloadOutlined, PlayCircleOutlined, SettingOutlined, StopOutlined, UploadOutlined} from '@ant-design/icons';
|
||||||
import '../App.css';
|
import '../App.css';
|
||||||
import {
|
import {
|
||||||
ExportData,
|
|
||||||
GetNetworkInterfaces,
|
GetNetworkInterfaces,
|
||||||
ReadRawJsonFile,
|
ReadRawJsonFile,
|
||||||
|
SaveParsedDataToDatabase,
|
||||||
StartCapture,
|
StartCapture,
|
||||||
StopAndParseCapture
|
StopAndParseCapture
|
||||||
} from '../../wailsjs/go/service/App';
|
} from '../../wailsjs/go/service/App';
|
||||||
@@ -170,50 +170,102 @@ function CapturePage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const exportData = async () => {
|
const exportData = () => {
|
||||||
if (!capturedData.length) {
|
// 检查是否有解析数据
|
||||||
|
if (!parsedData || !Array.isArray(parsedData.items) || parsedData.items.length === 0) {
|
||||||
showMessage('warning', '没有数据可导出');
|
showMessage('warning', '没有数据可导出');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const filename = `equipment_data_${Date.now()}.json`;
|
// 创建要导出的数据内容
|
||||||
const result = await safeApiCall(
|
const exportContent = JSON.stringify(parsedData, null, 2);
|
||||||
() => ExportData(capturedData, filename),
|
|
||||||
'导出数据失败'
|
// 创建 Blob 对象
|
||||||
);
|
const blob = new Blob([exportContent], { type: 'text/plain;charset=utf-8' });
|
||||||
if (result === undefined) {
|
|
||||||
showMessage('error', '导出数据失败');
|
// 创建下载链接
|
||||||
return;
|
const url = URL.createObjectURL(blob);
|
||||||
}
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = 'gear.txt';
|
||||||
|
|
||||||
|
// 触发下载
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
showMessage('success', '数据导出成功');
|
showMessage('success', '数据导出成功');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('导出数据时发生未知错误:', error);
|
console.error('导出数据时发生错误:', error);
|
||||||
showMessage('error', '导出数据失败');
|
showMessage('error', '导出数据失败');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
setUploadedFileName(file.name);
|
setUploadedFileName(file.name);
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = async (e) => {
|
||||||
try {
|
try {
|
||||||
const text = e.target?.result as string;
|
const text = e.target?.result as string;
|
||||||
const json = JSON.parse(text);
|
const json = JSON.parse(text);
|
||||||
|
|
||||||
|
// 校验数据格式
|
||||||
|
if (!json || typeof json !== 'object') {
|
||||||
|
showMessage('error', '文件格式错误:不是有效的JSON对象');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!json.heroes || !Array.isArray(json.heroes)) {
|
||||||
|
showMessage('error', '数据格式错误:缺少heroes数组');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!json.items || !Array.isArray(json.items)) {
|
||||||
|
showMessage('error', '数据格式错误:缺少items数组');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.heroes.length === 0) {
|
||||||
|
showMessage('error', '数据格式错误:heroes数组不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.items.length === 0) {
|
||||||
|
showMessage('error', '数据格式错误:items数组不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据校验通过,保存到数据库
|
||||||
|
const sessionName = `import_${Date.now()}`;
|
||||||
|
const equipmentJSON = JSON.stringify(json.items);
|
||||||
|
const heroesJSON = JSON.stringify(json.heroes);
|
||||||
|
|
||||||
|
await SaveParsedDataToDatabase(sessionName, equipmentJSON, heroesJSON);
|
||||||
|
|
||||||
|
// 更新界面显示
|
||||||
const safeData = {
|
const safeData = {
|
||||||
items: Array.isArray(json.items) ? json.items : [],
|
items: json.items,
|
||||||
heroes: Array.isArray(json.heroes) ? json.heroes : []
|
heroes: json.heroes
|
||||||
};
|
};
|
||||||
setParsedData(safeData);
|
setParsedData(safeData);
|
||||||
if (safeData.items.length === 0 && safeData.heroes.length === 0) {
|
setUploadedFileName(''); // 清空文件名显示
|
||||||
showMessage('warning', '上传文件数据为空,请检查文件内容');
|
|
||||||
} else {
|
showMessage('success', `数据导入成功:${json.items.length}件装备,${json.heroes.length}个英雄,已保存到数据库`);
|
||||||
showMessage('success', `文件解析成功:${safeData.items.length}件装备,${safeData.heroes.length}个英雄`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('文件格式错误,无法解析:', err);
|
console.error('文件处理错误:', err);
|
||||||
showMessage('error', '文件格式错误,无法解析');
|
if (err instanceof SyntaxError) {
|
||||||
|
showMessage('error', '文件格式错误:不是有效的JSON格式');
|
||||||
|
} else {
|
||||||
|
showMessage('error', '数据导入失败');
|
||||||
|
}
|
||||||
setParsedData({ items: [], heroes: [] });
|
setParsedData({ items: [], heroes: [] });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -315,19 +367,6 @@ function CapturePage() {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
style={{ flex: 1, height: 32 }}
|
style={{ flex: 1, height: 32 }}
|
||||||
>刷新数据</Button>
|
>刷新数据</Button>
|
||||||
<Button style={{ flex: 1, height: 32 }} onClick={handleUploadButtonClick}>
|
|
||||||
上传JSON
|
|
||||||
</Button>
|
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
accept="application/json"
|
|
||||||
style={{ display: 'none' }}
|
|
||||||
onChange={handleFileUpload}
|
|
||||||
/>
|
|
||||||
{uploadedFileName && (
|
|
||||||
<span style={{ marginLeft: 8, color: '#888', fontSize: 12 }}>{uploadedFileName}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Header>
|
</Header>
|
||||||
@@ -379,10 +418,28 @@ function CapturePage() {
|
|||||||
<Button
|
<Button
|
||||||
icon={<DownloadOutlined />}
|
icon={<DownloadOutlined />}
|
||||||
onClick={exportData}
|
onClick={exportData}
|
||||||
disabled={!capturedData.length}
|
disabled={!parsedData || !Array.isArray(parsedData.items) || parsedData.items.length === 0}
|
||||||
style={{ width: '100%', height: 32, fontSize: 14 }}>
|
style={{ width: '100%', height: 32, fontSize: 14 }}>
|
||||||
导出数据
|
导出数据
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon={<UploadOutlined />}
|
||||||
|
onClick={handleUploadButtonClick}
|
||||||
|
style={{ width: '100%', height: 32, fontSize: 14 }}>
|
||||||
|
导入数据
|
||||||
|
</Button>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".json,.txt"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
/>
|
||||||
|
{uploadedFileName && (
|
||||||
|
<span style={{ marginTop: 4, color: '#888', fontSize: 10, textAlign: 'center', display: 'block' }}>
|
||||||
|
{uploadedFileName}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
203
frontend/src/pages/DatabasePage.tsx
Normal file
203
frontend/src/pages/DatabasePage.tsx
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import React, {useEffect, useState} from 'react';
|
||||||
|
import {Button, Card, Col, Row, Space, Statistic, Table, Tag,} from 'antd';
|
||||||
|
import {BarChartOutlined, DatabaseOutlined, ReloadOutlined, SettingOutlined,} from '@ant-design/icons';
|
||||||
|
import * as App from '../../wailsjs/go/service/App';
|
||||||
|
import {model} from '../../wailsjs/go/models';
|
||||||
|
import {useMessage} from '../utils/useMessage';
|
||||||
|
|
||||||
|
// 定义Equipment接口
|
||||||
|
interface Equipment {
|
||||||
|
id: string | number;
|
||||||
|
code: string;
|
||||||
|
ct: number;
|
||||||
|
e: number;
|
||||||
|
g: number;
|
||||||
|
l: boolean;
|
||||||
|
mg: number;
|
||||||
|
op: any[];
|
||||||
|
p: number;
|
||||||
|
s: string;
|
||||||
|
sk: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DatabasePage: React.FC = () => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [latestData, setLatestData] = useState<model.ParsedResult | null>(null);
|
||||||
|
const { success, error, info } = useMessage();
|
||||||
|
|
||||||
|
// 加载最新解析数据
|
||||||
|
const loadLatestData = async () => {
|
||||||
|
// 防止重复调用
|
||||||
|
if (loading) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const parsedResult = await App.GetLatestParsedDataFromDatabase();
|
||||||
|
if (parsedResult && (parsedResult.items?.length > 0 || parsedResult.heroes?.length > 0)) {
|
||||||
|
setLatestData(parsedResult);
|
||||||
|
success('数据加载成功');
|
||||||
|
} else {
|
||||||
|
setLatestData(null);
|
||||||
|
info('暂无解析数据');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error('加载数据失败');
|
||||||
|
console.error('Load data error:', err);
|
||||||
|
setLatestData(null);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadLatestData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 装备表格列定义
|
||||||
|
const equipmentColumns = [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
width: 80,
|
||||||
|
render: (id: any) => {
|
||||||
|
const idStr = String(id || '');
|
||||||
|
return <span style={{ fontSize: '12px' }}>{idStr.length > 8 ? `${idStr.slice(0, 8)}...` : idStr}</span>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '代码',
|
||||||
|
dataIndex: 'code',
|
||||||
|
key: 'code',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '等级',
|
||||||
|
dataIndex: 'g',
|
||||||
|
key: 'g',
|
||||||
|
width: 80,
|
||||||
|
render: (grade: number) => (
|
||||||
|
<Tag color={grade >= 5 ? 'red' : grade >= 3 ? 'orange' : 'green'}>
|
||||||
|
{grade}
|
||||||
|
</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '经验',
|
||||||
|
dataIndex: 'e',
|
||||||
|
key: 'e',
|
||||||
|
width: 100,
|
||||||
|
render: (exp: number) => exp.toLocaleString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '力量',
|
||||||
|
dataIndex: 'p',
|
||||||
|
key: 'p',
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '魔法',
|
||||||
|
dataIndex: 'mg',
|
||||||
|
key: 'mg',
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '技能',
|
||||||
|
dataIndex: 'sk',
|
||||||
|
key: 'sk',
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'l',
|
||||||
|
key: 'l',
|
||||||
|
width: 80,
|
||||||
|
render: (locked: boolean) => (
|
||||||
|
<Tag color={locked ? 'red' : 'green'}>
|
||||||
|
{locked ? '锁定' : '正常'}
|
||||||
|
</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '24px' }}>
|
||||||
|
{/* 统计卡片 */}
|
||||||
|
<Row gutter={16} style={{ marginBottom: '24px' }}>
|
||||||
|
<Col span={8}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="数据库状态"
|
||||||
|
value="SQLite"
|
||||||
|
prefix={<DatabaseOutlined />}
|
||||||
|
suffix="已连接"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="装备数量"
|
||||||
|
value={latestData?.items?.length || 0}
|
||||||
|
prefix={<BarChartOutlined />}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="英雄数量"
|
||||||
|
value={latestData?.heroes?.length || 0}
|
||||||
|
prefix={<SettingOutlined />}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{/* 操作栏 */}
|
||||||
|
<Card style={{ marginBottom: '16px' }}>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
onClick={loadLatestData}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
刷新数据
|
||||||
|
</Button>
|
||||||
|
<span style={{ color: '#666' }}>
|
||||||
|
显示最新一次抓包解析的数据
|
||||||
|
</span>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 装备表格 */}
|
||||||
|
<Card title="最新解析的装备数据">
|
||||||
|
{latestData?.items && latestData.items.length > 0 ? (
|
||||||
|
<Table
|
||||||
|
columns={equipmentColumns}
|
||||||
|
dataSource={latestData.items}
|
||||||
|
rowKey="id"
|
||||||
|
loading={loading}
|
||||||
|
pagination={{
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total, range) =>
|
||||||
|
`第 ${range[0]}-${range[1]} 条,共 ${total} 条`,
|
||||||
|
}}
|
||||||
|
scroll={{ x: 800 }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||||
|
<p>暂无解析数据</p>
|
||||||
|
<p style={{ color: '#999', fontSize: '12px' }}>
|
||||||
|
请先进行抓包操作,解析后的数据会自动保存到数据库
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DatabasePage;
|
||||||
66
frontend/src/utils/useMessage.ts
Normal file
66
frontend/src/utils/useMessage.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import {message} from 'antd';
|
||||||
|
import {useRef} from 'react';
|
||||||
|
|
||||||
|
// 全局消息缓存,用于防止重复显示
|
||||||
|
const messageCache = new Map<string, number>();
|
||||||
|
const DEBOUNCE_TIME = 2000; // 2秒内相同消息不重复显示
|
||||||
|
|
||||||
|
export const useMessage = () => {
|
||||||
|
const messageKeyRef = useRef<string>('');
|
||||||
|
|
||||||
|
const showMessage = (type: 'success' | 'error' | 'warning' | 'info', content: string, duration?: number) => {
|
||||||
|
// 生成消息的唯一标识
|
||||||
|
const messageId = `${type}:${content}`;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// 检查是否在防抖时间内
|
||||||
|
const lastTime = messageCache.get(messageId);
|
||||||
|
if (lastTime && now - lastTime < DEBOUNCE_TIME) {
|
||||||
|
return; // 跳过重复消息
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新缓存
|
||||||
|
messageCache.set(messageId, now);
|
||||||
|
|
||||||
|
// 清理过期的缓存项(超过10秒的)
|
||||||
|
const expiredKeys: string[] = [];
|
||||||
|
messageCache.forEach((timestamp, msgId) => {
|
||||||
|
if (now - timestamp > 10000) {
|
||||||
|
expiredKeys.push(msgId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expiredKeys.forEach(key => messageCache.delete(key));
|
||||||
|
|
||||||
|
// 显示消息
|
||||||
|
message[type](content, duration);
|
||||||
|
};
|
||||||
|
|
||||||
|
const success = (content: string, duration?: number) => {
|
||||||
|
showMessage('success', content, duration);
|
||||||
|
};
|
||||||
|
|
||||||
|
const error = (content: string, duration?: number) => {
|
||||||
|
showMessage('error', content, duration);
|
||||||
|
};
|
||||||
|
|
||||||
|
const warning = (content: string, duration?: number) => {
|
||||||
|
showMessage('warning', content, duration);
|
||||||
|
};
|
||||||
|
|
||||||
|
const info = (content: string, duration?: number) => {
|
||||||
|
showMessage('info', content, duration);
|
||||||
|
};
|
||||||
|
|
||||||
|
const destroy = () => {
|
||||||
|
message.destroy();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
success,
|
||||||
|
error,
|
||||||
|
warning,
|
||||||
|
info,
|
||||||
|
destroy,
|
||||||
|
showMessage,
|
||||||
|
};
|
||||||
|
};
|
||||||
14
frontend/wailsjs/go/service/App.d.ts
vendored
14
frontend/wailsjs/go/service/App.d.ts
vendored
@@ -2,18 +2,32 @@
|
|||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
import {model} from '../models';
|
import {model} from '../models';
|
||||||
|
|
||||||
|
export function ExportCurrentData(arg1:string):Promise<void>;
|
||||||
|
|
||||||
export function ExportData(arg1:Array<string>,arg2:string):Promise<void>;
|
export function ExportData(arg1:Array<string>,arg2:string):Promise<void>;
|
||||||
|
|
||||||
|
export function GetAllAppSettings():Promise<Record<string, string>>;
|
||||||
|
|
||||||
|
export function GetAppSetting(arg1:string):Promise<string>;
|
||||||
|
|
||||||
export function GetCaptureStatus():Promise<model.CaptureStatus>;
|
export function GetCaptureStatus():Promise<model.CaptureStatus>;
|
||||||
|
|
||||||
export function GetCapturedData():Promise<Array<string>>;
|
export function GetCapturedData():Promise<Array<string>>;
|
||||||
|
|
||||||
|
export function GetCurrentDataForExport():Promise<string>;
|
||||||
|
|
||||||
|
export function GetLatestParsedDataFromDatabase():Promise<model.ParsedResult>;
|
||||||
|
|
||||||
export function GetNetworkInterfaces():Promise<Array<model.NetworkInterface>>;
|
export function GetNetworkInterfaces():Promise<Array<model.NetworkInterface>>;
|
||||||
|
|
||||||
export function ParseData(arg1:Array<string>):Promise<string>;
|
export function ParseData(arg1:Array<string>):Promise<string>;
|
||||||
|
|
||||||
export function ReadRawJsonFile():Promise<model.ParsedResult>;
|
export function ReadRawJsonFile():Promise<model.ParsedResult>;
|
||||||
|
|
||||||
|
export function SaveAppSetting(arg1:string,arg2:string):Promise<void>;
|
||||||
|
|
||||||
|
export function SaveParsedDataToDatabase(arg1:string,arg2:string,arg3:string):Promise<void>;
|
||||||
|
|
||||||
export function StartCapture(arg1:string):Promise<void>;
|
export function StartCapture(arg1:string):Promise<void>;
|
||||||
|
|
||||||
export function StopAndParseCapture():Promise<model.ParsedResult>;
|
export function StopAndParseCapture():Promise<model.ParsedResult>;
|
||||||
|
|||||||
@@ -2,10 +2,22 @@
|
|||||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
export function ExportCurrentData(arg1) {
|
||||||
|
return window['go']['service']['App']['ExportCurrentData'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function ExportData(arg1, arg2) {
|
export function ExportData(arg1, arg2) {
|
||||||
return window['go']['service']['App']['ExportData'](arg1, arg2);
|
return window['go']['service']['App']['ExportData'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function GetAllAppSettings() {
|
||||||
|
return window['go']['service']['App']['GetAllAppSettings']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetAppSetting(arg1) {
|
||||||
|
return window['go']['service']['App']['GetAppSetting'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function GetCaptureStatus() {
|
export function GetCaptureStatus() {
|
||||||
return window['go']['service']['App']['GetCaptureStatus']();
|
return window['go']['service']['App']['GetCaptureStatus']();
|
||||||
}
|
}
|
||||||
@@ -14,6 +26,14 @@ export function GetCapturedData() {
|
|||||||
return window['go']['service']['App']['GetCapturedData']();
|
return window['go']['service']['App']['GetCapturedData']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function GetCurrentDataForExport() {
|
||||||
|
return window['go']['service']['App']['GetCurrentDataForExport']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetLatestParsedDataFromDatabase() {
|
||||||
|
return window['go']['service']['App']['GetLatestParsedDataFromDatabase']();
|
||||||
|
}
|
||||||
|
|
||||||
export function GetNetworkInterfaces() {
|
export function GetNetworkInterfaces() {
|
||||||
return window['go']['service']['App']['GetNetworkInterfaces']();
|
return window['go']['service']['App']['GetNetworkInterfaces']();
|
||||||
}
|
}
|
||||||
@@ -26,6 +46,14 @@ export function ReadRawJsonFile() {
|
|||||||
return window['go']['service']['App']['ReadRawJsonFile']();
|
return window['go']['service']['App']['ReadRawJsonFile']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SaveAppSetting(arg1, arg2) {
|
||||||
|
return window['go']['service']['App']['SaveAppSetting'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SaveParsedDataToDatabase(arg1, arg2, arg3) {
|
||||||
|
return window['go']['service']['App']['SaveParsedDataToDatabase'](arg1, arg2, arg3);
|
||||||
|
}
|
||||||
|
|
||||||
export function StartCapture(arg1) {
|
export function StartCapture(arg1) {
|
||||||
return window['go']['service']['App']['StartCapture'](arg1);
|
return window['go']['service']['App']['StartCapture'](arg1);
|
||||||
}
|
}
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -6,6 +6,7 @@ toolchain go1.24.4
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/gopacket v1.1.19
|
github.com/google/gopacket v1.1.19
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.17
|
||||||
github.com/wailsapp/wails/v2 v2.10.1
|
github.com/wailsapp/wails/v2 v2.10.1
|
||||||
go.uber.org/zap v1.26.0
|
go.uber.org/zap v1.26.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -34,6 +34,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
|||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func GetNetworkInterfaces() ([]model.NetworkInterface, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("网卡名称: %s, 描述: %s", device.Name, device.Description) // 打印每个网卡的名称和描述
|
//log.Printf("网卡名称: %s, 描述: %s", device.Name, device.Description) // 打印每个网卡的名称和描述
|
||||||
|
|
||||||
// 提取IP地址
|
// 提取IP地址
|
||||||
var addresses []string
|
var addresses []string
|
||||||
|
|||||||
163
internal/model/database.go
Normal file
163
internal/model/database.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Database 数据库管理器
|
||||||
|
type Database struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDatabase 创建新的数据库连接
|
||||||
|
func NewDatabase() (*Database, error) {
|
||||||
|
dbPath := getDatabasePath()
|
||||||
|
|
||||||
|
// 确保目录存在
|
||||||
|
dir := filepath.Dir(dbPath)
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("创建数据库目录失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接数据库
|
||||||
|
db, err := sql.Open("sqlite3", dbPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("连接数据库失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试连接
|
||||||
|
if err := db.Ping(); err != nil {
|
||||||
|
return nil, fmt.Errorf("数据库连接测试失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
database := &Database{db: db}
|
||||||
|
|
||||||
|
// 初始化表结构
|
||||||
|
if err := database.initTables(); err != nil {
|
||||||
|
return nil, fmt.Errorf("初始化数据库表失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return database, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close 关闭数据库连接
|
||||||
|
func (d *Database) Close() error {
|
||||||
|
return d.db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDatabasePath 获取数据库文件路径
|
||||||
|
func getDatabasePath() string {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "equipment_analyzer.db"
|
||||||
|
}
|
||||||
|
return filepath.Join(homeDir, ".equipment-analyzer", "equipment_analyzer.db")
|
||||||
|
}
|
||||||
|
|
||||||
|
// initTables 初始化数据库表结构
|
||||||
|
func (d *Database) initTables() error {
|
||||||
|
// 解析数据表 - 存储抓包解析后的装备和角色数据
|
||||||
|
parsedDataTable := `
|
||||||
|
CREATE TABLE IF NOT EXISTS parsed_data (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
session_name TEXT NOT NULL,
|
||||||
|
items_json TEXT NOT NULL,
|
||||||
|
heroes_json TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
);`
|
||||||
|
|
||||||
|
// 应用设置表
|
||||||
|
settingsTable := `
|
||||||
|
CREATE TABLE IF NOT EXISTS app_settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
);`
|
||||||
|
|
||||||
|
tables := []string{
|
||||||
|
parsedDataTable,
|
||||||
|
settingsTable,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, table := range tables {
|
||||||
|
if _, err := d.db.Exec(table); err != nil {
|
||||||
|
return fmt.Errorf("创建表失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveParsedData 保存解析后的数据
|
||||||
|
func (d *Database) SaveParsedData(sessionName string, itemsJSON, heroesJSON string) error {
|
||||||
|
stmt := `
|
||||||
|
INSERT INTO parsed_data (session_name, items_json, heroes_json, created_at)
|
||||||
|
VALUES (?, ?, ?, ?)`
|
||||||
|
|
||||||
|
_, err := d.db.Exec(stmt, sessionName, itemsJSON, heroesJSON, time.Now().Unix())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLatestParsedData 获取最新的解析数据
|
||||||
|
func (d *Database) GetLatestParsedData() (string, string, error) {
|
||||||
|
stmt := `
|
||||||
|
SELECT items_json, heroes_json
|
||||||
|
FROM parsed_data
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 1`
|
||||||
|
|
||||||
|
var itemsJSON, heroesJSON string
|
||||||
|
err := d.db.QueryRow(stmt).Scan(&itemsJSON, &heroesJSON)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemsJSON, heroesJSON, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// SaveSetting 保存应用设置
|
||||||
|
func (d *Database) SaveSetting(key, value string) error {
|
||||||
|
stmt := "INSERT OR REPLACE INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)"
|
||||||
|
_, err := d.db.Exec(stmt, key, value, time.Now().Unix())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSetting 获取应用设置
|
||||||
|
func (d *Database) GetSetting(key string) (string, error) {
|
||||||
|
stmt := "SELECT value FROM app_settings WHERE key = ?"
|
||||||
|
var value string
|
||||||
|
err := d.db.QueryRow(stmt, key).Scan(&value)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllSettings 获取所有设置
|
||||||
|
func (d *Database) GetAllSettings() (map[string]string, error) {
|
||||||
|
stmt := "SELECT key, value FROM app_settings"
|
||||||
|
rows, err := d.db.Query(stmt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
settings := make(map[string]string)
|
||||||
|
for rows.Next() {
|
||||||
|
var key, value string
|
||||||
|
if err := rows.Scan(&key, &value); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
settings[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings, nil
|
||||||
|
}
|
||||||
82
internal/service/database_service.go
Normal file
82
internal/service/database_service.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"equipment-analyzer/internal/model"
|
||||||
|
"equipment-analyzer/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DatabaseService 数据库服务
|
||||||
|
type DatabaseService struct {
|
||||||
|
db *model.Database
|
||||||
|
logger *utils.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDatabaseService 创建数据库服务
|
||||||
|
func NewDatabaseService(db *model.Database, logger *utils.Logger) *DatabaseService {
|
||||||
|
return &DatabaseService{
|
||||||
|
db: db,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveParsedDataToDatabase 保存解析后的数据到数据库
|
||||||
|
func (s *DatabaseService) SaveParsedDataToDatabase(sessionName string, itemsJSON, heroesJSON string) error {
|
||||||
|
err := s.db.SaveParsedData(sessionName, itemsJSON, heroesJSON)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("保存解析数据到数据库失败", "error", err, "session_name", sessionName)
|
||||||
|
return fmt.Errorf("保存解析数据失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("解析数据保存成功", "session_name", sessionName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLatestParsedDataFromDatabase 从数据库获取最新的解析数据
|
||||||
|
func (s *DatabaseService) GetLatestParsedDataFromDatabase() (string, string, error) {
|
||||||
|
itemsJSON, heroesJSON, err := s.db.GetLatestParsedData()
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("从数据库获取最新解析数据失败", "error", err)
|
||||||
|
return "", "", fmt.Errorf("获取解析数据失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("最新解析数据获取成功")
|
||||||
|
return itemsJSON, heroesJSON, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveAppSetting 保存应用设置
|
||||||
|
func (s *DatabaseService) SaveAppSetting(key, value string) error {
|
||||||
|
err := s.db.SaveSetting(key, value)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("保存应用设置失败", "error", err, "key", key)
|
||||||
|
return fmt.Errorf("保存设置失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("应用设置保存成功", "key", key)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAppSetting 获取应用设置
|
||||||
|
func (s *DatabaseService) GetAppSetting(key string) (string, error) {
|
||||||
|
value, err := s.db.GetSetting(key)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取应用设置失败", "error", err, "key", key)
|
||||||
|
return "", fmt.Errorf("获取设置失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllAppSettings 获取所有应用设置
|
||||||
|
func (s *DatabaseService) GetAllAppSettings() (map[string]string, error) {
|
||||||
|
settings, err := s.db.GetAllSettings()
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取所有应用设置失败", "error", err)
|
||||||
|
return nil, fmt.Errorf("获取设置失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2,8 +2,8 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"equipment-analyzer/internal/capture"
|
"equipment-analyzer/internal/capture"
|
||||||
@@ -13,18 +13,37 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
config *config.Config
|
config *config.Config
|
||||||
logger *utils.Logger
|
logger *utils.Logger
|
||||||
captureService *CaptureService
|
captureService *CaptureService
|
||||||
parserService *ParserService
|
parserService *ParserService
|
||||||
|
database *model.Database
|
||||||
|
databaseService *DatabaseService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApp(cfg *config.Config, logger *utils.Logger) *App {
|
func NewApp(cfg *config.Config, logger *utils.Logger) *App {
|
||||||
|
// 初始化数据库
|
||||||
|
database, err := model.NewDatabase()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("初始化数据库失败", "error", err)
|
||||||
|
// 如果数据库初始化失败,仍然创建应用,但数据库功能不可用
|
||||||
|
return &App{
|
||||||
|
config: cfg,
|
||||||
|
logger: logger,
|
||||||
|
captureService: NewCaptureService(cfg, logger),
|
||||||
|
parserService: NewParserService(cfg, logger),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
databaseService := NewDatabaseService(database, logger)
|
||||||
|
|
||||||
return &App{
|
return &App{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
captureService: NewCaptureService(cfg, logger),
|
captureService: NewCaptureService(cfg, logger),
|
||||||
parserService: NewParserService(cfg, logger),
|
parserService: NewParserService(cfg, logger),
|
||||||
|
database: database,
|
||||||
|
databaseService: databaseService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,6 +62,15 @@ func (a *App) BeforeClose(ctx context.Context) (prevent bool) {
|
|||||||
|
|
||||||
func (a *App) Shutdown(ctx context.Context) {
|
func (a *App) Shutdown(ctx context.Context) {
|
||||||
a.logger.Info("应用关闭")
|
a.logger.Info("应用关闭")
|
||||||
|
|
||||||
|
// 关闭数据库连接
|
||||||
|
if a.database != nil {
|
||||||
|
if err := a.database.Close(); err != nil {
|
||||||
|
a.logger.Error("关闭数据库连接失败", "error", err)
|
||||||
|
} else {
|
||||||
|
a.logger.Info("数据库连接已关闭")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNetworkInterfaces 获取网络接口列表
|
// GetNetworkInterfaces 获取网络接口列表
|
||||||
@@ -130,6 +158,80 @@ func (a *App) ExportData(hexDataList []string, filename string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExportCurrentData 导出当前数据库中的数据到文件
|
||||||
|
func (a *App) ExportCurrentData(filename string) error {
|
||||||
|
if a.databaseService == nil {
|
||||||
|
return fmt.Errorf("数据库服务未初始化")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从数据库获取最新数据
|
||||||
|
parsedResult, err := a.GetLatestParsedDataFromDatabase()
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Error("获取数据库数据失败", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedResult == nil || (len(parsedResult.Items) == 0 && len(parsedResult.Heroes) == 0) {
|
||||||
|
return fmt.Errorf("没有数据可导出")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建导出数据格式
|
||||||
|
exportData := map[string]interface{}{
|
||||||
|
"items": parsedResult.Items,
|
||||||
|
"heroes": parsedResult.Heroes,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 序列化为JSON
|
||||||
|
jsonData, err := json.MarshalIndent(exportData, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Error("序列化数据失败", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入文件
|
||||||
|
err = utils.WriteFile(filename, jsonData)
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Error("写入文件失败", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.logger.Info("数据导出成功", "filename", filename, "items_count", len(parsedResult.Items), "heroes_count", len(parsedResult.Heroes))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentDataForExport 获取当前数据库中的数据,供前端导出使用
|
||||||
|
func (a *App) GetCurrentDataForExport() (string, error) {
|
||||||
|
if a.databaseService == nil {
|
||||||
|
return "", fmt.Errorf("数据库服务未初始化")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从数据库获取最新数据
|
||||||
|
parsedResult, err := a.GetLatestParsedDataFromDatabase()
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Error("获取数据库数据失败", "error", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedResult == nil || (len(parsedResult.Items) == 0 && len(parsedResult.Heroes) == 0) {
|
||||||
|
return "", fmt.Errorf("没有数据可导出")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建导出数据格式
|
||||||
|
exportData := map[string]interface{}{
|
||||||
|
"items": parsedResult.Items,
|
||||||
|
"heroes": parsedResult.Heroes,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 序列化为JSON
|
||||||
|
jsonData, err := json.MarshalIndent(exportData, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Error("序列化数据失败", "error", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(jsonData), nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetCaptureStatus 获取抓包状态
|
// GetCaptureStatus 获取抓包状态
|
||||||
func (a *App) GetCaptureStatus() model.CaptureStatus {
|
func (a *App) GetCaptureStatus() model.CaptureStatus {
|
||||||
return model.CaptureStatus{
|
return model.CaptureStatus{
|
||||||
@@ -145,13 +247,9 @@ func (a *App) getStatusMessage() string {
|
|||||||
return "准备就绪"
|
return "准备就绪"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadRawJsonFile 供前端调用,读取output_raw.json内容
|
// ReadRawJsonFile 已废弃,请使用GetLatestParsedDataFromDatabase从数据库获取数据
|
||||||
func (a *App) ReadRawJsonFile() (*model.ParsedResult, error) {
|
func (a *App) ReadRawJsonFile() (*model.ParsedResult, error) {
|
||||||
data, err := ioutil.ReadFile("output_raw.json")
|
return a.GetLatestParsedDataFromDatabase()
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return a.parserService.ReadRawJsonFile(string(data))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StopAndParseCapture 停止抓包并解析数据,供前端调用
|
// StopAndParseCapture 停止抓包并解析数据,供前端调用
|
||||||
@@ -161,5 +259,101 @@ func (a *App) StopAndParseCapture() (*model.ParsedResult, error) {
|
|||||||
a.logger.Error("停止抓包并解析数据失败", "error", err)
|
a.logger.Error("停止抓包并解析数据失败", "error", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 将解析结果保存到数据库
|
||||||
|
if a.databaseService != nil && result != nil {
|
||||||
|
// 序列化装备数据
|
||||||
|
itemsJSON := "[]"
|
||||||
|
if result.Items != nil {
|
||||||
|
if jsonData, err := json.Marshal(result.Items); err == nil {
|
||||||
|
itemsJSON = string(jsonData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 序列化英雄数据
|
||||||
|
heroesJSON := "[]"
|
||||||
|
if result.Heroes != nil {
|
||||||
|
if jsonData, err := json.Marshal(result.Heroes); err == nil {
|
||||||
|
heroesJSON = string(jsonData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到数据库
|
||||||
|
sessionName := fmt.Sprintf("capture_%d", time.Now().Unix())
|
||||||
|
if err := a.databaseService.SaveParsedDataToDatabase(sessionName, itemsJSON, heroesJSON); err != nil {
|
||||||
|
a.logger.Error("保存解析数据到数据库失败", "error", err)
|
||||||
|
// 不返回错误,因为解析成功了,只是保存失败
|
||||||
|
} else {
|
||||||
|
a.logger.Info("解析数据已保存到数据库", "session_name", sessionName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 数据库相关API ==========
|
||||||
|
|
||||||
|
// SaveParsedDataToDatabase 保存解析后的数据到数据库
|
||||||
|
func (a *App) SaveParsedDataToDatabase(sessionName string, itemsJSON, heroesJSON string) error {
|
||||||
|
if a.databaseService == nil {
|
||||||
|
return fmt.Errorf("数据库服务未初始化")
|
||||||
|
}
|
||||||
|
return a.databaseService.SaveParsedDataToDatabase(sessionName, itemsJSON, heroesJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLatestParsedDataFromDatabase 从数据库获取最新的解析数据
|
||||||
|
func (a *App) GetLatestParsedDataFromDatabase() (*model.ParsedResult, error) {
|
||||||
|
if a.databaseService == nil {
|
||||||
|
return nil, fmt.Errorf("数据库服务未初始化")
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsJSON, heroesJSON, err := a.databaseService.GetLatestParsedDataFromDatabase()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析装备数据
|
||||||
|
var items []interface{}
|
||||||
|
if itemsJSON != "" {
|
||||||
|
if err := json.Unmarshal([]byte(itemsJSON), &items); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析装备数据失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析英雄数据
|
||||||
|
var heroes []interface{}
|
||||||
|
if heroesJSON != "" {
|
||||||
|
if err := json.Unmarshal([]byte(heroesJSON), &heroes); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析英雄数据失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.ParsedResult{
|
||||||
|
Items: items,
|
||||||
|
Heroes: heroes,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveAppSetting 保存应用设置
|
||||||
|
func (a *App) SaveAppSetting(key, value string) error {
|
||||||
|
if a.databaseService == nil {
|
||||||
|
return fmt.Errorf("数据库服务未初始化")
|
||||||
|
}
|
||||||
|
return a.databaseService.SaveAppSetting(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAppSetting 获取应用设置
|
||||||
|
func (a *App) GetAppSetting(key string) (string, error) {
|
||||||
|
if a.databaseService == nil {
|
||||||
|
return "", fmt.Errorf("数据库服务未初始化")
|
||||||
|
}
|
||||||
|
return a.databaseService.GetAppSetting(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllAppSettings 获取所有应用设置
|
||||||
|
func (a *App) GetAllAppSettings() (map[string]string, error) {
|
||||||
|
if a.databaseService == nil {
|
||||||
|
return nil, fmt.Errorf("数据库服务未初始化")
|
||||||
|
}
|
||||||
|
return a.databaseService.GetAllAppSettings()
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -67,12 +67,8 @@ func (ps *ParserService) ParseHexData(hexDataList []string) (*model.ParsedResult
|
|||||||
return nil, "", fmt.Errorf("远程json校验失败,data字段缺失或为空")
|
return nil, "", fmt.Errorf("远程json校验失败,data字段缺失或为空")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验通过再写入本地文件
|
// 校验通过,直接解析数据
|
||||||
fileErr := ioutil.WriteFile("output_raw.json", body, 0644)
|
ps.logger.Info("远程原始数据校验通过,开始解析")
|
||||||
if fileErr != nil {
|
|
||||||
ps.logger.Error("写入原始json文件失败", "error", fileErr)
|
|
||||||
}
|
|
||||||
ps.logger.Info("远程原始数据已写入output_raw.json")
|
|
||||||
parsedResult, err := ps.ReadRawJsonFile(string(body))
|
parsedResult, err := ps.ReadRawJsonFile(string(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
|
|||||||
@@ -1,85 +1,13 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"equipment-analyzer/internal/config"
|
|
||||||
"equipment-analyzer/internal/utils"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestReadRawJsonFile(t *testing.T) {
|
func TestReadRawJsonFile(t *testing.T) {
|
||||||
// 创建测试用的配置和日志器
|
// 此测试已废弃,因为ReadRawJsonFile方法已被移除
|
||||||
config := &config.Config{}
|
// 现在数据存储在SQLite数据库中,不再依赖output_raw.json文件
|
||||||
logger := utils.NewLogger()
|
t.Skip("ReadRawJsonFile测试已废弃,数据现在存储在SQLite数据库中")
|
||||||
|
|
||||||
// 创建ParserService实例
|
|
||||||
ps := NewParserService(config, logger)
|
|
||||||
|
|
||||||
// 调用ReadRawJsonFile方法
|
|
||||||
result, err := ps.ReadRawJsonFile()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ReadRawJsonFile failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Raw result length: %d\n", len(result))
|
|
||||||
fmt.Printf("Raw result preview: %s\n", result[:min(200, len(result))])
|
|
||||||
|
|
||||||
// 解析JSON结果
|
|
||||||
var parsedData map[string]interface{}
|
|
||||||
if err := json.Unmarshal([]byte(result), &parsedData); err != nil {
|
|
||||||
t.Fatalf("Failed to parse JSON result: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查数据结构
|
|
||||||
fmt.Printf("Parsed data keys: %v\n", getKeys(parsedData))
|
|
||||||
|
|
||||||
// 检查items字段
|
|
||||||
if items, exists := parsedData["items"]; exists {
|
|
||||||
if itemsArray, ok := items.([]interface{}); ok {
|
|
||||||
fmt.Printf("Items count: %d\n", len(itemsArray))
|
|
||||||
if len(itemsArray) > 0 {
|
|
||||||
fmt.Printf("First item: %+v\n", itemsArray[0])
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Printf("Items is not an array: %T\n", items)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Println("Items field not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查heroes字段
|
|
||||||
if heroes, exists := parsedData["heroes"]; exists {
|
|
||||||
if heroesArray, ok := heroes.([]interface{}); ok {
|
|
||||||
fmt.Printf("Heroes count: %d\n", len(heroesArray))
|
|
||||||
if len(heroesArray) > 0 {
|
|
||||||
fmt.Printf("First hero: %+v\n", heroesArray[0])
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Printf("Heroes is not an array: %T\n", heroes)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Println("Heroes field not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有数据,输出更多调试信息
|
|
||||||
if len(result) < 100 {
|
|
||||||
fmt.Printf("Result seems empty or very short: %q\n", result)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助函数
|
// 辅助函数已移除,因为测试已废弃
|
||||||
func min(a, b int) int {
|
|
||||||
if a < b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func getKeys(m map[string]interface{}) []string {
|
|
||||||
keys := make([]string, 0, len(m))
|
|
||||||
for k := range m {
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
return keys
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ type Logger struct {
|
|||||||
func NewLogger() *Logger {
|
func NewLogger() *Logger {
|
||||||
// 配置日志轮转
|
// 配置日志轮转
|
||||||
lumberJackLogger := &lumberjack.Logger{
|
lumberJackLogger := &lumberjack.Logger{
|
||||||
Filename: "logs/equipment-analyzer.log",
|
Filename: "./logs/equipment-analyzer.log",
|
||||||
MaxSize: 100, // MB
|
MaxSize: 100, // MB
|
||||||
MaxBackups: 3,
|
MaxBackups: 3,
|
||||||
MaxAge: 28, // days
|
MaxAge: 28, // days
|
||||||
|
|||||||
126588
opepic.json
126588
opepic.json
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
134423
output_raw_e7.json
134423
output_raw_e7.json
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user