feat(database): add gearTxt field to parsed results and update related functions
This commit is contained in:
@@ -3,6 +3,7 @@ import {Button, Card, Layout, message, Spin, Table} from 'antd';
|
||||
import {DownloadOutlined, PlayCircleOutlined, StopOutlined, UploadOutlined} from '@ant-design/icons';
|
||||
import '../App.css';
|
||||
import {
|
||||
ParseAndSaveRawJson,
|
||||
ReadRawJsonFile,
|
||||
SaveParsedDataToDatabase,
|
||||
StartCaptureWithFilter,
|
||||
@@ -239,40 +240,22 @@ function CapturePage() {
|
||||
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 gearTxt = typeof text === 'string' ? text : JSON.stringify(json);
|
||||
const parsed = await ParseAndSaveRawJson(sessionName, gearTxt);
|
||||
|
||||
const safeData = {
|
||||
items: json.items,
|
||||
heroes: json.heroes
|
||||
items: parsed?.items || [],
|
||||
heroes: parsed?.heroes || []
|
||||
};
|
||||
setParsedData(safeData);
|
||||
setUploadedFileName('');
|
||||
|
||||
showMessage('success', `导入成功:${json.items.length}件装备,${json.heroes.length}个英雄`);
|
||||
if (safeData.items.length === 0 || safeData.heroes.length === 0) {
|
||||
showMessage('warning', `导入完成,但解析为空:${safeData.items.length}件装备,${safeData.heroes.length}个英雄`);
|
||||
} else {
|
||||
showMessage('success', `导入成功:${safeData.items.length}件装备,${safeData.heroes.length}个英雄`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('文件处理错误:', err);
|
||||
if (err instanceof SyntaxError) {
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Button, Card, Col, Input, Layout, Modal, Row, Select, Space, Statistic, Table, Tag} from 'antd';
|
||||
import {BarChartOutlined, DatabaseOutlined, ReloadOutlined, SettingOutlined,} from '@ant-design/icons';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Card, Col, Input, Layout, Modal, Row, Select, Space, Statistic, Table } from 'antd';
|
||||
import { BarChartOutlined, DatabaseOutlined, DownloadOutlined, 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';
|
||||
import { model } from '../../wailsjs/go/models';
|
||||
import { useMessage } from '../utils/useMessage';
|
||||
|
||||
const { Content } = Layout;
|
||||
|
||||
// 定义Equipment接口
|
||||
// 定义 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;
|
||||
id?: string | number;
|
||||
code?: string;
|
||||
set?: string;
|
||||
level?: number;
|
||||
enhance?: number;
|
||||
gear?: string;
|
||||
main?: { type?: string; value?: number };
|
||||
substats?: Array<{ type?: string; value?: number }>;
|
||||
ingameEquippedId?: string;
|
||||
p?: string | number;
|
||||
}
|
||||
|
||||
const DatabasePage: React.FC = () => {
|
||||
@@ -29,8 +28,123 @@ const DatabasePage: React.FC = () => {
|
||||
const [selectedSessionId, setSelectedSessionId] = useState<number | null>(null);
|
||||
const [renameOpen, setRenameOpen] = useState(false);
|
||||
const [renameValue, setRenameValue] = useState('');
|
||||
const [setFilter, setSetFilter] = useState<string | null>(null);
|
||||
const [gearFilter, setGearFilter] = useState<string | null>(null);
|
||||
const [mainStatFilter, setMainStatFilter] = useState<string | null>(null);
|
||||
const { success, error, info } = useMessage();
|
||||
|
||||
const SET_LABELS: Record<string, string> = {
|
||||
AttackSet: '攻击套装',
|
||||
DefenseSet: '防御套装',
|
||||
HealthSet: '生命值套装',
|
||||
SpeedSet: '速度套装',
|
||||
CriticalSet: '暴击套装',
|
||||
DestructionSet: '破灭套装',
|
||||
HitSet: '命中套装',
|
||||
ResistSet: '抵抗套装',
|
||||
LifestealSet: '吸血套装',
|
||||
CounterSet: '反击套装',
|
||||
ImmunitySet: '免疫套装',
|
||||
PenetrationSet: '穿透套装',
|
||||
InjurySet: '伤口套装',
|
||||
ProtectionSet: '守护套装',
|
||||
TorrentSet: '激流套装',
|
||||
ReversalSet: '逆袭套装',
|
||||
RiposteSet: '回击套装',
|
||||
WarfareSet: '开战套装',
|
||||
PursuitSet: '追击套装',
|
||||
RageSet: '愤怒套装',
|
||||
RevengeSet: '憎恨套装',
|
||||
UnitySet: '夹攻套装',
|
||||
};
|
||||
|
||||
const GEAR_LABELS: Record<string, string> = {
|
||||
Weapon: '武器',
|
||||
Helmet: '头盔',
|
||||
Armor: '铠甲',
|
||||
Necklace: '项链',
|
||||
Ring: '戒指',
|
||||
Boots: '鞋子',
|
||||
};
|
||||
|
||||
const STAT_LABELS: Record<string, string> = {
|
||||
Attack: '攻击力',
|
||||
Defense: '防御力',
|
||||
Health: '生命值',
|
||||
Speed: '速度',
|
||||
AttackPercent: '攻击力%',
|
||||
DefensePercent: '防御力%',
|
||||
HealthPercent: '生命值%',
|
||||
CriticalHitChancePercent: '暴击率',
|
||||
CriticalHitDamagePercent: '暴击伤害',
|
||||
EffectivenessPercent: '效果命中',
|
||||
EffectResistancePercent: '效果抗性',
|
||||
};
|
||||
|
||||
const PERCENT_STATS = new Set([
|
||||
'AttackPercent',
|
||||
'DefensePercent',
|
||||
'HealthPercent',
|
||||
'CriticalHitChancePercent',
|
||||
'CriticalHitDamagePercent',
|
||||
'EffectivenessPercent',
|
||||
'EffectResistancePercent',
|
||||
]);
|
||||
|
||||
const formatStat = (stat?: { type?: string; value?: number }) => {
|
||||
if (!stat || !stat.type) return '-';
|
||||
const rawLabel = STAT_LABELS[stat.type] || stat.type;
|
||||
const value = stat.value ?? 0;
|
||||
const rawValue = Number.isFinite(value) ? value : 0;
|
||||
const roundedValue = Math.round(rawValue * 10) / 10;
|
||||
const formattedValue = Number.isInteger(roundedValue) ? roundedValue.toString() : roundedValue.toString();
|
||||
if (PERCENT_STATS.has(stat.type)) {
|
||||
const label = rawLabel.endsWith('%') ? rawLabel.slice(0, -1) : rawLabel;
|
||||
return `${label}${formattedValue}%`;
|
||||
}
|
||||
return `${rawLabel} ${formattedValue}`;
|
||||
};
|
||||
|
||||
const formatSubstats = (subs?: Array<{ type?: string; value?: number }>) => {
|
||||
if (!subs || subs.length === 0) return '-';
|
||||
return subs.slice(0, 4).map(s => formatStat(s)).join(' / ');
|
||||
};
|
||||
|
||||
const heroNameById = React.useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
const heroes = latestData?.heroes || [];
|
||||
heroes.forEach((h: any) => {
|
||||
const id = h?.id ?? h?.ingameId ?? h?.code;
|
||||
if (id !== undefined && id !== null) {
|
||||
const key = String(id);
|
||||
map.set(key, h?.name || h?.code || key);
|
||||
}
|
||||
});
|
||||
return map;
|
||||
}, [latestData?.heroes]);
|
||||
|
||||
const getEquippedHeroName = (item: Equipment) => {
|
||||
const key = item.ingameEquippedId ?? item.p;
|
||||
if (key === undefined || key === null || key === '') return '-';
|
||||
return heroNameById.get(String(key)) || String(key);
|
||||
};
|
||||
|
||||
const filteredItems = React.useMemo(() => {
|
||||
const items = latestData?.items || [];
|
||||
return items.filter((item: Equipment) => {
|
||||
if (setFilter) {
|
||||
if ((item.set || '') !== setFilter) return false;
|
||||
}
|
||||
if (gearFilter) {
|
||||
if ((item.gear || '') !== gearFilter) return false;
|
||||
}
|
||||
if (mainStatFilter) {
|
||||
if ((item.main?.type || '') !== mainStatFilter) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [latestData?.items, setFilter, gearFilter, mainStatFilter]);
|
||||
|
||||
const formatSessionLabel = (session: model.ParsedSession) => {
|
||||
const date = session.created_at ? new Date(session.created_at * 1000) : null;
|
||||
const timeText = date ? date.toLocaleString() : '';
|
||||
@@ -88,9 +202,33 @@ const DatabasePage: React.FC = () => {
|
||||
loadSessions();
|
||||
};
|
||||
|
||||
const exportData = () => {
|
||||
if (!latestData?.geartxt) {
|
||||
info('没有数据可导出');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const exportContent = latestData.geartxt;
|
||||
const blob = new Blob([exportContent], { type: 'text/plain;charset=utf-8' });
|
||||
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);
|
||||
success('数据导出成功');
|
||||
} catch (err) {
|
||||
console.error('导出数据出错:', err);
|
||||
error('数据导出失败');
|
||||
}
|
||||
};
|
||||
|
||||
const openRename = () => {
|
||||
if (!selectedSessionId) {
|
||||
info('请先选择一条解析数据');
|
||||
info('请先选择一条装备记录');
|
||||
return;
|
||||
}
|
||||
const current = sessions.find(s => s.id === selectedSessionId);
|
||||
@@ -124,7 +262,7 @@ const DatabasePage: React.FC = () => {
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!selectedSessionId) {
|
||||
info('请先选择一条解析数据');
|
||||
info('请先选择一条装备记录');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
@@ -147,7 +285,7 @@ const DatabasePage: React.FC = () => {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -158,67 +296,46 @@ const DatabasePage: React.FC = () => {
|
||||
// 装备表格列定义
|
||||
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',
|
||||
title: '套装类型',
|
||||
dataIndex: 'set',
|
||||
key: 'set',
|
||||
width: 120,
|
||||
render: (_: any, record: Equipment) => SET_LABELS[record.set || ''] || record.set || '-',
|
||||
},
|
||||
{
|
||||
title: '等级',
|
||||
dataIndex: 'g',
|
||||
key: 'g',
|
||||
dataIndex: 'level',
|
||||
key: 'level',
|
||||
width: 80,
|
||||
render: (grade: number) => (
|
||||
<Tag color={grade >= 5 ? 'red' : grade >= 3 ? 'orange' : 'green'}>
|
||||
{grade}
|
||||
</Tag>
|
||||
),
|
||||
render: (value: number) => (value ?? '-'),
|
||||
},
|
||||
{
|
||||
title: '经验',
|
||||
dataIndex: 'e',
|
||||
key: 'e',
|
||||
title: '强化等级',
|
||||
dataIndex: 'enhance',
|
||||
key: 'enhance',
|
||||
width: 90,
|
||||
render: (value: number) => (value === undefined || value === null ? '-' : `+${value}`),
|
||||
},
|
||||
{
|
||||
title: '部位',
|
||||
dataIndex: 'gear',
|
||||
key: 'gear',
|
||||
width: 100,
|
||||
render: (exp: number) => exp.toLocaleString(),
|
||||
render: (gear: string) => GEAR_LABELS[gear] || gear || '-',
|
||||
},
|
||||
{
|
||||
title: '力量',
|
||||
dataIndex: 'p',
|
||||
key: 'p',
|
||||
width: 80,
|
||||
title: '主属性',
|
||||
dataIndex: 'main',
|
||||
key: 'main',
|
||||
width: 180,
|
||||
render: (_: any, record: Equipment) => formatStat(record.main),
|
||||
},
|
||||
{
|
||||
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>
|
||||
),
|
||||
title: '副属性',
|
||||
dataIndex: 'substats',
|
||||
key: 'substats',
|
||||
width: 320,
|
||||
render: (_: any, record: Equipment) => formatSubstats(record.substats),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -268,6 +385,7 @@ const DatabasePage: React.FC = () => {
|
||||
>
|
||||
刷新数据
|
||||
</Button>
|
||||
<Button icon={<DownloadOutlined />} onClick={exportData} disabled={!latestData?.geartxt}>导出数据</Button>
|
||||
<Select
|
||||
style={{ minWidth: 320 }}
|
||||
placeholder="请选择解析数据"
|
||||
@@ -287,11 +405,57 @@ const DatabasePage: React.FC = () => {
|
||||
</Card>
|
||||
|
||||
{/* 装备表格 */}
|
||||
<Card title="解析的装备数据">
|
||||
<Card
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<span>解析的装备数据</span>
|
||||
<Space>
|
||||
<span>套装类型</span>
|
||||
<Select
|
||||
style={{ minWidth: 180 }}
|
||||
placeholder="全部"
|
||||
value={setFilter ?? undefined}
|
||||
allowClear
|
||||
onChange={value => setSetFilter(value ?? null)}
|
||||
options={Object.keys(SET_LABELS).map(key => ({
|
||||
value: key,
|
||||
label: SET_LABELS[key],
|
||||
}))}
|
||||
/>
|
||||
<span>部位</span>
|
||||
<Select
|
||||
style={{ minWidth: 140 }}
|
||||
placeholder="全部"
|
||||
value={gearFilter ?? undefined}
|
||||
allowClear
|
||||
onChange={value => setGearFilter(value ?? null)}
|
||||
options={Object.keys(GEAR_LABELS).map(key => ({
|
||||
value: key,
|
||||
label: GEAR_LABELS[key],
|
||||
}))}
|
||||
/>
|
||||
<span>主属性</span>
|
||||
<Select
|
||||
style={{ minWidth: 160 }}
|
||||
placeholder="全部"
|
||||
value={mainStatFilter ?? undefined}
|
||||
allowClear
|
||||
onChange={value => setMainStatFilter(value ?? null)}
|
||||
options={Object.keys(STAT_LABELS)
|
||||
.filter(key => !['Attack', 'Defense', 'Health'].includes(key))
|
||||
.map(key => ({
|
||||
value: key,
|
||||
label: STAT_LABELS[key],
|
||||
}))}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{latestData?.items && latestData.items.length > 0 ? (
|
||||
<Table
|
||||
columns={equipmentColumns}
|
||||
dataSource={latestData.items}
|
||||
dataSource={filteredItems}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
@@ -332,3 +496,4 @@ const DatabasePage: React.FC = () => {
|
||||
};
|
||||
|
||||
export default DatabasePage;
|
||||
|
||||
|
||||
@@ -321,6 +321,7 @@ export namespace model {
|
||||
export class ParsedResult {
|
||||
items: any[];
|
||||
heroes: any[];
|
||||
geartxt: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new ParsedResult(source);
|
||||
@@ -330,6 +331,7 @@ export namespace model {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.items = source["items"];
|
||||
this.heroes = source["heroes"];
|
||||
this.geartxt = source["geartxt"];
|
||||
}
|
||||
}
|
||||
export class ParsedSession {
|
||||
|
||||
4
frontend/wailsjs/go/service/App.d.ts
vendored
4
frontend/wailsjs/go/service/App.d.ts
vendored
@@ -30,13 +30,15 @@ export function GetParsedSessions():Promise<Array<model.ParsedSession>>;
|
||||
|
||||
export function OptimizeBuilds(arg1:model.OptimizeRequest):Promise<model.OptimizeResponse>;
|
||||
|
||||
export function ParseAndSaveRawJson(arg1:string,arg2:string):Promise<model.ParsedResult>;
|
||||
|
||||
export function ParseData(arg1:Array<string>):Promise<string>;
|
||||
|
||||
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 SaveParsedDataToDatabase(arg1:string,arg2:string,arg3:string,arg4:string):Promise<void>;
|
||||
|
||||
export function StartCapture(arg1:string):Promise<void>;
|
||||
|
||||
|
||||
@@ -58,6 +58,10 @@ export function OptimizeBuilds(arg1) {
|
||||
return window['go']['service']['App']['OptimizeBuilds'](arg1);
|
||||
}
|
||||
|
||||
export function ParseAndSaveRawJson(arg1, arg2) {
|
||||
return window['go']['service']['App']['ParseAndSaveRawJson'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function ParseData(arg1) {
|
||||
return window['go']['service']['App']['ParseData'](arg1);
|
||||
}
|
||||
@@ -70,8 +74,8 @@ 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 SaveParsedDataToDatabase(arg1, arg2, arg3, arg4) {
|
||||
return window['go']['service']['App']['SaveParsedDataToDatabase'](arg1, arg2, arg3, arg4);
|
||||
}
|
||||
|
||||
export function StartCapture(arg1) {
|
||||
|
||||
19
go.mod
19
go.mod
@@ -1,6 +1,6 @@
|
||||
module equipment-analyzer
|
||||
|
||||
go 1.22.0
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.4
|
||||
|
||||
@@ -9,7 +9,7 @@ require (
|
||||
github.com/wailsapp/wails/v2 v2.11.0
|
||||
go.uber.org/zap v1.26.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
modernc.org/sqlite v1.29.0
|
||||
modernc.org/sqlite v1.45.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -19,7 +19,6 @@ require (
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
@@ -29,7 +28,7 @@ require (
|
||||
github.com/leaanthony/u v1.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
@@ -42,13 +41,11 @@ require (
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
||||
modernc.org/libc v1.41.0 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.7.2 // indirect
|
||||
modernc.org/strutil v1.2.0 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
modernc.org/libc v1.67.6 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
66
go.sum
66
go.sum
@@ -10,8 +10,8 @@ github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
@@ -42,10 +42,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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
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/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@@ -83,18 +81,20 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
|
||||
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -103,8 +103,8 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -112,24 +112,38 @@ golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
|
||||
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||
modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk=
|
||||
modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||
modernc.org/sqlite v1.29.0 h1:lQVw+ZsFM3aRG5m4myG70tbXpr3S/J1ej0KHIP4EvjM=
|
||||
modernc.org/sqlite v1.29.0/go.mod h1:hG41jCYxOAOoO6BRK66AdRlmOcDzXf7qnwlwjUIOqa0=
|
||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
||||
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
||||
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
|
||||
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.45.0 h1:r51cSGzKpbptxnby+EIIz5fop4VuE4qFoVEjNvWoObs=
|
||||
modernc.org/sqlite v1.45.0/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
|
||||
@@ -6,59 +6,60 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// Database 数据库管理器
|
||||
// Database manages the app database.
|
||||
type Database struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// NewDatabase 创建新的数据库连接
|
||||
// NewDatabase creates a new database connection.
|
||||
func NewDatabase() (*Database, error) {
|
||||
dbPath := getDatabasePath()
|
||||
log.Printf("[db] init: path=%s", dbPath)
|
||||
|
||||
// 确保目录存在
|
||||
// Ensure directory exists.
|
||||
dir := filepath.Dir(dbPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
log.Printf("[db] mkdir failed: dir=%s err=%v", dir, err)
|
||||
return nil, fmt.Errorf("创建数据库目录失败: %w", err)
|
||||
return nil, fmt.Errorf("create database dir failed: %w", err)
|
||||
}
|
||||
|
||||
// 连接数据库
|
||||
// Connect database.
|
||||
db, err := sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
log.Printf("[db] open failed: path=%s err=%v", dbPath, err)
|
||||
return nil, fmt.Errorf("连接数据库失败: %w", err)
|
||||
return nil, fmt.Errorf("open database failed: %w", err)
|
||||
}
|
||||
|
||||
// 测试连接
|
||||
// Test connection.
|
||||
if err := db.Ping(); err != nil {
|
||||
log.Printf("[db] ping failed: err=%v", err)
|
||||
return nil, fmt.Errorf("数据库连接测试失败: %w", err)
|
||||
return nil, fmt.Errorf("database ping failed: %w", err)
|
||||
}
|
||||
|
||||
database := &Database{db: db}
|
||||
|
||||
// 初始化表结构
|
||||
// Init tables.
|
||||
if err := database.initTables(); err != nil {
|
||||
log.Printf("[db] init tables failed: err=%v", err)
|
||||
return nil, fmt.Errorf("初始化数据库表失败: %w", err)
|
||||
return nil, fmt.Errorf("init tables failed: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("[db] init ok")
|
||||
return database, nil
|
||||
}
|
||||
|
||||
// Close 关闭数据库连接
|
||||
// Close closes the database connection.
|
||||
func (d *Database) Close() error {
|
||||
return d.db.Close()
|
||||
}
|
||||
|
||||
// getDatabasePath 获取数据库文件路径
|
||||
// getDatabasePath returns the database file path.
|
||||
func getDatabasePath() string {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
@@ -67,19 +68,18 @@ func getDatabasePath() string {
|
||||
return filepath.Join(homeDir, ".equipment-analyzer", "equipment_analyzer.db")
|
||||
}
|
||||
|
||||
// initTables 初始化数据库表结构
|
||||
// initTables creates tables if not exist.
|
||||
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,
|
||||
geartxt TEXT NOT NULL DEFAULT '',
|
||||
created_at INTEGER NOT NULL
|
||||
);`
|
||||
|
||||
// 应用设置表
|
||||
settingsTable := `
|
||||
CREATE TABLE IF NOT EXISTS app_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
@@ -94,44 +94,59 @@ func (d *Database) initTables() error {
|
||||
|
||||
for _, table := range tables {
|
||||
if _, err := d.db.Exec(table); err != nil {
|
||||
return fmt.Errorf("创建表失败: %w", err)
|
||||
return fmt.Errorf("create table failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := d.ensureParsedDataColumns(); err != nil {
|
||||
return 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 (?, ?, ?, ?)`
|
||||
func (d *Database) ensureParsedDataColumns() error {
|
||||
_, err := d.db.Exec("ALTER TABLE parsed_data ADD COLUMN geartxt TEXT NOT NULL DEFAULT ''")
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "duplicate column name") {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("add geartxt column failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := d.db.Exec(stmt, sessionName, itemsJSON, heroesJSON, time.Now().Unix())
|
||||
// SaveParsedData saves parsed items/heroes with raw gear txt.
|
||||
func (d *Database) SaveParsedData(sessionName string, itemsJSON, heroesJSON, gearTxt string) error {
|
||||
stmt := `
|
||||
INSERT INTO parsed_data (session_name, items_json, heroes_json, geartxt, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)`
|
||||
|
||||
_, err := d.db.Exec(stmt, sessionName, itemsJSON, heroesJSON, gearTxt, time.Now().Unix())
|
||||
return err
|
||||
}
|
||||
|
||||
// GetLatestParsedData 获取最新的解析数据
|
||||
func (d *Database) GetLatestParsedData() (string, string, error) {
|
||||
// GetLatestParsedData returns latest parsed data.
|
||||
func (d *Database) GetLatestParsedData() (string, string, string, error) {
|
||||
stmt := `
|
||||
SELECT items_json, heroes_json
|
||||
SELECT items_json, heroes_json, geartxt
|
||||
FROM parsed_data
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1`
|
||||
|
||||
var itemsJSON, heroesJSON string
|
||||
err := d.db.QueryRow(stmt).Scan(&itemsJSON, &heroesJSON)
|
||||
var itemsJSON, heroesJSON, gearTxt string
|
||||
err := d.db.QueryRow(stmt).Scan(&itemsJSON, &heroesJSON, &gearTxt)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return "", "", nil
|
||||
return "", "", "", nil
|
||||
}
|
||||
return "", "", err
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
return itemsJSON, heroesJSON, nil
|
||||
return itemsJSON, heroesJSON, gearTxt, nil
|
||||
}
|
||||
|
||||
// GetParsedSessions 获取所有解析会话
|
||||
// GetParsedSessions returns all parsed sessions.
|
||||
func (d *Database) GetParsedSessions() ([]ParsedSession, error) {
|
||||
stmt := `
|
||||
SELECT id, session_name, created_at
|
||||
@@ -155,26 +170,26 @@ func (d *Database) GetParsedSessions() ([]ParsedSession, error) {
|
||||
return sessions, nil
|
||||
}
|
||||
|
||||
// GetParsedDataByID 获取指定会话的数据
|
||||
func (d *Database) GetParsedDataByID(id int64) (string, string, error) {
|
||||
// GetParsedDataByID returns parsed data for a session.
|
||||
func (d *Database) GetParsedDataByID(id int64) (string, string, string, error) {
|
||||
stmt := `
|
||||
SELECT items_json, heroes_json
|
||||
SELECT items_json, heroes_json, geartxt
|
||||
FROM parsed_data
|
||||
WHERE id = ?
|
||||
LIMIT 1`
|
||||
|
||||
var itemsJSON, heroesJSON string
|
||||
err := d.db.QueryRow(stmt, id).Scan(&itemsJSON, &heroesJSON)
|
||||
var itemsJSON, heroesJSON, gearTxt string
|
||||
err := d.db.QueryRow(stmt, id).Scan(&itemsJSON, &heroesJSON, &gearTxt)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return "", "", nil
|
||||
return "", "", "", nil
|
||||
}
|
||||
return "", "", err
|
||||
return "", "", "", err
|
||||
}
|
||||
return itemsJSON, heroesJSON, nil
|
||||
return itemsJSON, heroesJSON, gearTxt, nil
|
||||
}
|
||||
|
||||
// UpdateParsedSessionName 更新解析会话名称
|
||||
// UpdateParsedSessionName updates session name.
|
||||
func (d *Database) UpdateParsedSessionName(id int64, name string) error {
|
||||
stmt := `
|
||||
UPDATE parsed_data
|
||||
@@ -184,7 +199,7 @@ func (d *Database) UpdateParsedSessionName(id int64, name string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteParsedSession 删除解析会话
|
||||
// DeleteParsedSession deletes a parsed session.
|
||||
func (d *Database) DeleteParsedSession(id int64) error {
|
||||
stmt := `
|
||||
DELETE FROM parsed_data
|
||||
@@ -193,14 +208,14 @@ func (d *Database) DeleteParsedSession(id int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// SaveSetting 保存应用设置
|
||||
// SaveSetting saves app setting.
|
||||
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 获取应用设置
|
||||
// GetSetting returns app setting.
|
||||
func (d *Database) GetSetting(key string) (string, error) {
|
||||
stmt := "SELECT value FROM app_settings WHERE key = ?"
|
||||
var value string
|
||||
@@ -211,7 +226,7 @@ func (d *Database) GetSetting(key string) (string, error) {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// GetAllSettings 获取所有设置
|
||||
// GetAllSettings returns all settings.
|
||||
func (d *Database) GetAllSettings() (map[string]string, error) {
|
||||
stmt := "SELECT key, value FROM app_settings"
|
||||
rows, err := d.db.Query(stmt)
|
||||
|
||||
@@ -32,8 +32,9 @@ type CaptureStatus struct {
|
||||
|
||||
// ParsedResult 解析结果
|
||||
type ParsedResult struct {
|
||||
Items []interface{} `json:"items"`
|
||||
Heroes []interface{} `json:"heroes"`
|
||||
Items []interface{} `json:"items"`
|
||||
Heroes []interface{} `json:"heroes"`
|
||||
GearTxt string `json:"geartxt"`
|
||||
}
|
||||
|
||||
// ParsedSession 解析数据会话信息
|
||||
|
||||
@@ -22,8 +22,8 @@ func NewDatabaseService(db *model.Database, logger *utils.Logger) *DatabaseServi
|
||||
}
|
||||
|
||||
// SaveParsedDataToDatabase 保存解析后的数据到数据库
|
||||
func (s *DatabaseService) SaveParsedDataToDatabase(sessionName string, itemsJSON, heroesJSON string) error {
|
||||
err := s.db.SaveParsedData(sessionName, itemsJSON, heroesJSON)
|
||||
func (s *DatabaseService) SaveParsedDataToDatabase(sessionName string, itemsJSON, heroesJSON, gearTxt string) error {
|
||||
err := s.db.SaveParsedData(sessionName, itemsJSON, heroesJSON, gearTxt)
|
||||
if err != nil {
|
||||
s.logger.Error("保存解析数据到数据库失败", "error", err, "session_name", sessionName)
|
||||
return fmt.Errorf("保存解析数据失败: %w", err)
|
||||
@@ -34,15 +34,15 @@ func (s *DatabaseService) SaveParsedDataToDatabase(sessionName string, itemsJSON
|
||||
}
|
||||
|
||||
// GetLatestParsedDataFromDatabase 从数据库获取最新的解析数据
|
||||
func (s *DatabaseService) GetLatestParsedDataFromDatabase() (string, string, error) {
|
||||
itemsJSON, heroesJSON, err := s.db.GetLatestParsedData()
|
||||
func (s *DatabaseService) GetLatestParsedDataFromDatabase() (string, string, string, error) {
|
||||
itemsJSON, heroesJSON, gearTxt, err := s.db.GetLatestParsedData()
|
||||
if err != nil {
|
||||
s.logger.Error("从数据库获取最新解析数据失败", "error", err)
|
||||
return "", "", fmt.Errorf("获取解析数据失败: %w", err)
|
||||
return "", "", "", fmt.Errorf("获取解析数据失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("最新解析数据获取成功")
|
||||
return itemsJSON, heroesJSON, nil
|
||||
return itemsJSON, heroesJSON, gearTxt, nil
|
||||
}
|
||||
|
||||
// GetParsedSessions 从数据库获取所有解析会话
|
||||
@@ -56,13 +56,13 @@ func (s *DatabaseService) GetParsedSessions() ([]model.ParsedSession, error) {
|
||||
}
|
||||
|
||||
// GetParsedDataByID 从数据库获取指定会话数据
|
||||
func (s *DatabaseService) GetParsedDataByID(id int64) (string, string, error) {
|
||||
itemsJSON, heroesJSON, err := s.db.GetParsedDataByID(id)
|
||||
func (s *DatabaseService) GetParsedDataByID(id int64) (string, string, string, error) {
|
||||
itemsJSON, heroesJSON, gearTxt, err := s.db.GetParsedDataByID(id)
|
||||
if err != nil {
|
||||
s.logger.Error("从数据库获取解析数据失败", "error", err, "id", id)
|
||||
return "", "", fmt.Errorf("获取解析数据失败: %w", err)
|
||||
return "", "", "", fmt.Errorf("获取解析数据失败: %w", err)
|
||||
}
|
||||
return itemsJSON, heroesJSON, nil
|
||||
return itemsJSON, heroesJSON, gearTxt, nil
|
||||
}
|
||||
|
||||
// UpdateParsedSessionName 更新解析会话名称
|
||||
|
||||
@@ -365,7 +365,7 @@ func (a *App) StopAndParseCapture() (*model.ParsedResult, error) {
|
||||
}
|
||||
|
||||
sessionName := fmt.Sprintf("capture_%d", time.Now().Unix())
|
||||
if err := a.databaseService.SaveParsedDataToDatabase(sessionName, itemsJSON, heroesJSON); err != nil {
|
||||
if err := a.databaseService.SaveParsedDataToDatabase(sessionName, itemsJSON, heroesJSON, result.GearTxt); err != nil {
|
||||
a.logger.Error("save parsed data failed", "error", err)
|
||||
} else {
|
||||
a.logger.Info("parsed data saved", "session_name", sessionName)
|
||||
@@ -376,11 +376,42 @@ func (a *App) StopAndParseCapture() (*model.ParsedResult, error) {
|
||||
}
|
||||
|
||||
// SaveParsedDataToDatabase saves parsed data.
|
||||
func (a *App) SaveParsedDataToDatabase(sessionName string, itemsJSON, heroesJSON string) error {
|
||||
func (a *App) SaveParsedDataToDatabase(sessionName string, itemsJSON, heroesJSON, gearTxt string) error {
|
||||
if a.databaseService == nil {
|
||||
return fmt.Errorf("database service not initialized")
|
||||
}
|
||||
return a.databaseService.SaveParsedDataToDatabase(sessionName, itemsJSON, heroesJSON)
|
||||
return a.databaseService.SaveParsedDataToDatabase(sessionName, itemsJSON, heroesJSON, gearTxt)
|
||||
}
|
||||
|
||||
// ParseAndSaveRawJson parses raw gear json and saves to database.
|
||||
func (a *App) ParseAndSaveRawJson(sessionName string, rawJson string) (*model.ParsedResult, error) {
|
||||
if a.databaseService == nil {
|
||||
return nil, fmt.Errorf("database service not initialized")
|
||||
}
|
||||
if sessionName == "" {
|
||||
return nil, fmt.Errorf("session name cannot be empty")
|
||||
}
|
||||
result, err := a.parserService.ReadRawJsonFile(rawJson)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
if err := a.databaseService.SaveParsedDataToDatabase(sessionName, itemsJSON, heroesJSON, result.GearTxt); err != nil {
|
||||
a.logger.Error("save parsed data failed", "error", err, "session_name", sessionName)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetLatestParsedDataFromDatabase returns latest parsed data from database.
|
||||
@@ -389,7 +420,7 @@ func (a *App) GetLatestParsedDataFromDatabase() (*model.ParsedResult, error) {
|
||||
return nil, fmt.Errorf("database service not initialized")
|
||||
}
|
||||
|
||||
itemsJSON, heroesJSON, err := a.databaseService.GetLatestParsedDataFromDatabase()
|
||||
itemsJSON, heroesJSON, gearTxt, err := a.databaseService.GetLatestParsedDataFromDatabase()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -412,6 +443,7 @@ func (a *App) GetLatestParsedDataFromDatabase() (*model.ParsedResult, error) {
|
||||
return &model.ParsedResult{
|
||||
Items: items,
|
||||
Heroes: heroes,
|
||||
GearTxt: gearTxt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -428,7 +460,7 @@ func (a *App) GetParsedDataByID(id int64) (*model.ParsedResult, error) {
|
||||
if a.databaseService == nil {
|
||||
return nil, fmt.Errorf("database service not initialized")
|
||||
}
|
||||
itemsJSON, heroesJSON, err := a.databaseService.GetParsedDataByID(id)
|
||||
itemsJSON, heroesJSON, gearTxt, err := a.databaseService.GetParsedDataByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -450,6 +482,7 @@ func (a *App) GetParsedDataByID(id int64) (*model.ParsedResult, error) {
|
||||
return &model.ParsedResult{
|
||||
Items: items,
|
||||
Heroes: heroes,
|
||||
GearTxt: gearTxt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ type ParserService struct {
|
||||
heroBase map[string]heroBaseStats
|
||||
heroOnce sync.Once
|
||||
heroErr error
|
||||
fallbackMainStatCount int
|
||||
}
|
||||
|
||||
func NewParserService(cfg *config.Config, logger *utils.Logger) *ParserService {
|
||||
@@ -37,24 +38,43 @@ func NewParserService(cfg *config.Config, logger *utils.Logger) *ParserService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func (ps *ParserService) writeParseSnapshot(kind string, data []byte) {
|
||||
if len(data) == 0 {
|
||||
return
|
||||
}
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
dir := filepath.Join(homeDir, ".equipment-analyzer", "remote_parse_snapshots")
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return
|
||||
}
|
||||
stamp := time.Now().Format("20060102_150405.000")
|
||||
filename := filepath.Join(dir, stamp+"_"+kind+".json")
|
||||
_ = os.WriteFile(filename, data, 0644)
|
||||
}
|
||||
|
||||
// ParseHexData 解析十六进制数据
|
||||
func (ps *ParserService) ParseHexData(hexDataList []string) (*model.ParsedResult, string, error) {
|
||||
if len(hexDataList) == 0 {
|
||||
ps.logger.Warn("没有数据需要解析")
|
||||
ps.logger.Warn("no data to parse")
|
||||
return &model.ParsedResult{
|
||||
Items: make([]interface{}, 0),
|
||||
Heroes: make([]interface{}, 0),
|
||||
}, "", nil
|
||||
}
|
||||
|
||||
ps.logger.Info("开始远程解析数据", "count", len(hexDataList))
|
||||
ps.logger.Info("remote parse start", "count", len(hexDataList))
|
||||
|
||||
url := "https://krivpfvxi0.execute-api.us-west-2.amazonaws.com/dev/getItems"
|
||||
reqBody := map[string]interface{}{
|
||||
"data": hexDataList,
|
||||
}
|
||||
jsonBytes, _ := json.Marshal(reqBody)
|
||||
client := &http.Client{Timeout: 15 * time.Second}
|
||||
ps.writeParseSnapshot("request", jsonBytes)
|
||||
client := &http.Client{Timeout: 60 * time.Second}
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBytes))
|
||||
if err == nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
@@ -62,23 +82,21 @@ func (ps *ParserService) ParseHexData(hexDataList []string) (*model.ParsedResult
|
||||
if err == nil && resp.StatusCode == 200 {
|
||||
defer resp.Body.Close()
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
ps.writeParseSnapshot("response", body)
|
||||
|
||||
// 新校验逻辑:校验data和units字段
|
||||
var raw map[string]interface{}
|
||||
if err := json.Unmarshal(body, &raw); err != nil {
|
||||
ps.logger.Error("远程json解析失败", "error", err)
|
||||
return nil, "", fmt.Errorf("远程json解析失败: %v", err)
|
||||
ps.logger.Error("remote json unmarshal failed", "error", err)
|
||||
return nil, "", fmt.Errorf("remote json unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
// 校验data字段
|
||||
dataArr, dataOk := raw["data"].([]interface{})
|
||||
if !dataOk || len(dataArr) == 0 {
|
||||
ps.logger.Error("远程json校验失败,data字段缺失或为空")
|
||||
return nil, "", fmt.Errorf("远程json校验失败,data字段缺失或为空")
|
||||
ps.logger.Error("remote json validate failed: data missing or empty")
|
||||
return nil, "", fmt.Errorf("remote json validate failed: data missing or empty")
|
||||
}
|
||||
|
||||
// 校验通过,直接解析数据
|
||||
ps.logger.Info("远程原始数据校验通过,开始解析")
|
||||
ps.logger.Info("remote json validate ok, start parse")
|
||||
parsedResult, err := ps.ReadRawJsonFile(string(body))
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
@@ -86,15 +104,15 @@ func (ps *ParserService) ParseHexData(hexDataList []string) (*model.ParsedResult
|
||||
|
||||
return parsedResult, "", nil
|
||||
} else if err != nil {
|
||||
ps.logger.Error("远程解析请求失败", "error", err)
|
||||
return nil, "", fmt.Errorf("远程解析请求失败: %v", err)
|
||||
ps.logger.Error("remote parse request failed", "error", err)
|
||||
return nil, "", fmt.Errorf("remote parse request failed: %v", err)
|
||||
} else {
|
||||
ps.logger.Error("远程解析响应码异常", "status", resp.StatusCode)
|
||||
return nil, "", fmt.Errorf("远程解析响应码异常: %d", resp.StatusCode)
|
||||
ps.logger.Error("remote parse http status", "status", resp.StatusCode)
|
||||
return nil, "", fmt.Errorf("remote parse http status %d", resp.StatusCode)
|
||||
}
|
||||
} else {
|
||||
ps.logger.Error("远程解析请求构建失败", "error", err)
|
||||
return nil, "", fmt.Errorf("远程解析请求构建失败: %v", err)
|
||||
ps.logger.Error("remote parse request build failed", "error", err)
|
||||
return nil, "", fmt.Errorf("remote parse request build failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +124,62 @@ func (ps *ParserService) ReadRawJsonFile(rawJson string) (*model.ParsedResult, e
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 提取装备和英雄数据
|
||||
|
||||
// If input is already parsed (gear.txt export), use items/heroes directly.
|
||||
if itemsRaw, ok := rawData["items"].([]interface{}); ok {
|
||||
heroesRaw, _ := rawData["heroes"].([]interface{})
|
||||
parsedItems := ps.normalizeParsedItems(itemsRaw)
|
||||
result := &model.ParsedResult{
|
||||
Items: parsedItems,
|
||||
Heroes: heroesRaw,
|
||||
GearTxt: rawJson,
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// If input is a mixed "data" array (remote parse response), split items/heroes.
|
||||
if dataRaw, ok := rawData["data"].([]interface{}); ok && len(dataRaw) > 0 {
|
||||
dataItems, dataHeroes := ps.splitDataArray(dataRaw)
|
||||
if len(dataItems) > 0 || len(dataHeroes) > 0 {
|
||||
ps.logger.Info("raw json format: mixed data array", "items", len(dataItems), "heroes", len(dataHeroes))
|
||||
// If heroes are not present in data array, fall back to units.
|
||||
var rawUnits []interface{}
|
||||
if unitsRaw, ok := rawData["units"].([]interface{}); ok && len(unitsRaw) > 0 {
|
||||
maxLen := 0
|
||||
for _, u := range unitsRaw {
|
||||
if arr, ok := u.([]interface{}); ok && len(arr) > maxLen {
|
||||
maxLen = len(arr)
|
||||
rawUnits = arr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parsedHeroes := dataHeroes
|
||||
if len(rawUnits) > 0 {
|
||||
parsedHeroes = rawUnits
|
||||
}
|
||||
|
||||
ps.fallbackMainStatCount = 0
|
||||
convertedItems := ps.convertItemsAllWithLog(dataItems)
|
||||
ps.logger.Info("main stat fallback count", "count", ps.fallbackMainStatCount)
|
||||
convertedHeroes := ps.convertUnits(parsedHeroes)
|
||||
|
||||
result := &model.ParsedResult{
|
||||
Items: make([]interface{}, len(convertedItems)),
|
||||
Heroes: make([]interface{}, len(convertedHeroes)),
|
||||
}
|
||||
for i, v := range convertedItems {
|
||||
result.Items[i] = v
|
||||
}
|
||||
for i, v := range convertedHeroes {
|
||||
result.Heroes[i] = v
|
||||
}
|
||||
result.GearTxt = rawJson
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 提取装备和英雄数?
|
||||
equips, _ := rawData["data"].([]interface{})
|
||||
// 修正:units 取最大长度的那组
|
||||
var rawUnits []interface{}
|
||||
@@ -131,8 +204,10 @@ func (ps *ParserService) ReadRawJsonFile(rawJson string) (*model.ParsedResult, e
|
||||
}
|
||||
|
||||
// 转换装备数据
|
||||
ps.fallbackMainStatCount = 0
|
||||
convertedItems := ps.convertItemsAllWithLog(validEquips)
|
||||
// 转换英雄数据(只对最大组)
|
||||
ps.logger.Info("main stat fallback count", "count", ps.fallbackMainStatCount)
|
||||
// 转换英雄数据(只对最大组?
|
||||
convertedHeroes := ps.convertUnits(rawUnits)
|
||||
|
||||
result := &model.ParsedResult{
|
||||
@@ -145,10 +220,115 @@ func (ps *ParserService) ReadRawJsonFile(rawJson string) (*model.ParsedResult, e
|
||||
for i, v := range convertedHeroes {
|
||||
result.Heroes[i] = v
|
||||
}
|
||||
result.GearTxt = rawJson
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
|
||||
func (ps *ParserService) isGearRecord(itemMap map[string]interface{}) bool {
|
||||
// Require set field if present in this raw format.
|
||||
if f, ok := itemMap["f"].(string); ok && f != "" {
|
||||
return true
|
||||
}
|
||||
// Allow already-normalized gear fields from other sources.
|
||||
if _, ok := itemMap["set"]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := itemMap["gear"]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := itemMap["type"]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := itemMap["mainStatType"]; ok {
|
||||
return true
|
||||
}
|
||||
// Otherwise treat as non-gear to avoid including material-like records.
|
||||
return false
|
||||
}
|
||||
|
||||
func (ps *ParserService) isHeroRecord(itemMap map[string]interface{}) bool {
|
||||
if name, ok := itemMap["name"].(string); ok && name != "" && name != "Unknown" {
|
||||
return true
|
||||
}
|
||||
if code, ok := itemMap["code"].(string); ok && strings.HasPrefix(code, "c") {
|
||||
return true
|
||||
}
|
||||
// heroes commonly have opt/exp fields
|
||||
if _, ok := itemMap["opt"]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := itemMap["exp"]; ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
func (ps *ParserService) splitDataArray(data []interface{}) (items []interface{}, heroes []interface{}) {
|
||||
for _, entry := range data {
|
||||
itemMap, ok := entry.(map[string]interface{})
|
||||
if !ok || itemMap == nil {
|
||||
continue
|
||||
}
|
||||
if ps.isGearRecord(itemMap) {
|
||||
items = append(items, entry)
|
||||
continue
|
||||
}
|
||||
if ps.isHeroRecord(itemMap) {
|
||||
heroes = append(heroes, entry)
|
||||
continue
|
||||
}
|
||||
}
|
||||
return items, heroes
|
||||
}
|
||||
|
||||
|
||||
func (ps *ParserService) normalizeParsedItems(items []interface{}) []interface{} {
|
||||
for i, item := range items {
|
||||
itemMap, ok := item.(map[string]interface{})
|
||||
if !ok || itemMap == nil {
|
||||
continue
|
||||
}
|
||||
if _, exists := itemMap["main"]; exists {
|
||||
continue
|
||||
}
|
||||
op, opExists := itemMap["op"].([]interface{})
|
||||
if !opExists || len(op) == 0 {
|
||||
continue
|
||||
}
|
||||
mainOp, ok := op[0].([]interface{})
|
||||
if !ok || len(mainOp) < 2 {
|
||||
continue
|
||||
}
|
||||
mainOpType, _ := mainOp[0].(string)
|
||||
mainOpValue, _ := mainOp[1].(float64)
|
||||
if mainOpType == "" {
|
||||
continue
|
||||
}
|
||||
mainType := statByIngameStat[mainOpType]
|
||||
if mainType == "" {
|
||||
continue
|
||||
}
|
||||
var mainValue float64
|
||||
if ps.isFlat(mainOpType) {
|
||||
mainValue = mainOpValue
|
||||
} else {
|
||||
mainValue = ps.round10ths(mainOpValue * 100)
|
||||
}
|
||||
if mainValue == 0 || mainValue != mainValue {
|
||||
mainValue = 0
|
||||
}
|
||||
itemMap["main"] = map[string]interface{}{
|
||||
"type": mainType,
|
||||
"value": mainValue,
|
||||
}
|
||||
items[i] = itemMap
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// convertItems 转换装备数据
|
||||
func (ps *ParserService) convertItems(rawItems []interface{}) []map[string]interface{} {
|
||||
var convertedItems []map[string]interface{}
|
||||
@@ -197,10 +377,10 @@ func (ps *ParserService) convertSingleItem(item map[string]interface{}) map[stri
|
||||
// 转换增强
|
||||
ps.convertEnhance(converted)
|
||||
|
||||
// 转换主属性
|
||||
// 转换主属?
|
||||
ps.convertMainStat(converted)
|
||||
|
||||
// 转换副属性
|
||||
// 转换副属?
|
||||
ps.convertSubStats(converted)
|
||||
|
||||
// 转换ID
|
||||
@@ -227,7 +407,7 @@ func (ps *ParserService) convertUnits(rawUnits []interface{}) []map[string]inter
|
||||
convertedUnit[key] = value
|
||||
}
|
||||
|
||||
// 转换星星和觉醒
|
||||
// 转换星星和觉?
|
||||
if g, exists := unitMap["g"]; exists {
|
||||
convertedUnit["stars"] = g
|
||||
}
|
||||
@@ -335,6 +515,7 @@ func (ps *ParserService) convertEnhance(item map[string]interface{}) {
|
||||
func (ps *ParserService) convertMainStat(item map[string]interface{}) {
|
||||
op, opExists := item["op"].([]interface{})
|
||||
mainStatValue, mainStatExists := item["mainStatValue"].(float64)
|
||||
fallbackUsed := false
|
||||
|
||||
if opExists && len(op) > 0 && mainStatExists {
|
||||
if mainOp, ok := op[0].([]interface{}); ok && len(mainOp) > 0 {
|
||||
@@ -359,6 +540,39 @@ func (ps *ParserService) convertMainStat(item map[string]interface{}) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if main is missing, derive from op[0] value directly.
|
||||
if _, exists := item["main"]; !exists {
|
||||
if opExists && len(op) > 0 {
|
||||
if mainOp, ok := op[0].([]interface{}); ok && len(mainOp) >= 2 {
|
||||
mainOpType, _ := mainOp[0].(string)
|
||||
mainOpValue, _ := mainOp[1].(float64)
|
||||
if mainOpType != "" {
|
||||
mainType := statByIngameStat[mainOpType]
|
||||
if mainType != "" {
|
||||
var mainValue float64
|
||||
if ps.isFlat(mainOpType) {
|
||||
mainValue = mainOpValue
|
||||
} else {
|
||||
mainValue = ps.round10ths(mainOpValue * 100)
|
||||
}
|
||||
if mainValue == 0 || mainValue != mainValue {
|
||||
mainValue = 0
|
||||
}
|
||||
item["main"] = map[string]interface{}{
|
||||
"type": mainType,
|
||||
"value": mainValue,
|
||||
}
|
||||
fallbackUsed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fallbackUsed {
|
||||
ps.fallbackMainStatCount++
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *ParserService) convertSubStats(item map[string]interface{}) {
|
||||
@@ -371,7 +585,7 @@ func (ps *ParserService) convertSubStats(item map[string]interface{}) {
|
||||
|
||||
statAcc := make(map[string]map[string]interface{})
|
||||
|
||||
// 处理副属性 (从索引1开始)
|
||||
// 处理副属?(从索?开?
|
||||
for i := 1; i < len(op); i++ {
|
||||
if opItem, ok := op[i].([]interface{}); ok && len(opItem) >= 2 {
|
||||
opType, _ := opItem[0].(string)
|
||||
@@ -417,7 +631,7 @@ func (ps *ParserService) convertSubStats(item map[string]interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
// 转换为最终格式
|
||||
// 转换为最终格?
|
||||
var substats []interface{}
|
||||
for statType, statData := range statAcc {
|
||||
substat := map[string]interface{}{
|
||||
@@ -765,3 +979,4 @@ func (ps *ParserService) applyHeroBase(unit map[string]interface{}, base heroBas
|
||||
unit["res"] = base.Res
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user