feat(database): add gearTxt field to parsed results and update related functions

This commit is contained in:
kever
2026-02-17 22:42:59 +08:00
parent 5dba3d9930
commit 8c4c4e77d7
13 changed files with 658 additions and 226 deletions

View File

@@ -3,6 +3,7 @@ import {Button, Card, Layout, message, Spin, Table} from 'antd';
import {DownloadOutlined, PlayCircleOutlined, StopOutlined, UploadOutlined} from '@ant-design/icons'; import {DownloadOutlined, PlayCircleOutlined, StopOutlined, UploadOutlined} from '@ant-design/icons';
import '../App.css'; import '../App.css';
import { import {
ParseAndSaveRawJson,
ReadRawJsonFile, ReadRawJsonFile,
SaveParsedDataToDatabase, SaveParsedDataToDatabase,
StartCaptureWithFilter, StartCaptureWithFilter,
@@ -239,40 +240,22 @@ function CapturePage() {
return; 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 sessionName = `import_${Date.now()}`;
const equipmentJSON = JSON.stringify(json.items); const gearTxt = typeof text === 'string' ? text : JSON.stringify(json);
const heroesJSON = JSON.stringify(json.heroes); const parsed = await ParseAndSaveRawJson(sessionName, gearTxt);
await SaveParsedDataToDatabase(sessionName, equipmentJSON, heroesJSON);
const safeData = { const safeData = {
items: json.items, items: parsed?.items || [],
heroes: json.heroes heroes: parsed?.heroes || []
}; };
setParsedData(safeData); setParsedData(safeData);
setUploadedFileName(''); 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) { } catch (err) {
console.error('文件处理错误:', err); console.error('文件处理错误:', err);
if (err instanceof SyntaxError) { if (err instanceof SyntaxError) {

View File

@@ -1,6 +1,6 @@
import React, {useEffect, useState} from 'react'; import React, { useEffect, useState } from 'react';
import {Button, Card, Col, Input, Layout, Modal, Row, Select, Space, Statistic, Table, Tag} from 'antd'; import { Button, Card, Col, Input, Layout, Modal, Row, Select, Space, Statistic, Table } from 'antd';
import {BarChartOutlined, DatabaseOutlined, ReloadOutlined, SettingOutlined,} from '@ant-design/icons'; import { BarChartOutlined, DatabaseOutlined, DownloadOutlined, ReloadOutlined, SettingOutlined } from '@ant-design/icons';
import * as App from '../../wailsjs/go/service/App'; import * as App from '../../wailsjs/go/service/App';
import { model } from '../../wailsjs/go/models'; import { model } from '../../wailsjs/go/models';
import { useMessage } from '../utils/useMessage'; import { useMessage } from '../utils/useMessage';
@@ -9,17 +9,16 @@ const { Content } = Layout;
// 定义 Equipment 接口 // 定义 Equipment 接口
interface Equipment { interface Equipment {
id: string | number; id?: string | number;
code: string; code?: string;
ct: number; set?: string;
e: number; level?: number;
g: number; enhance?: number;
l: boolean; gear?: string;
mg: number; main?: { type?: string; value?: number };
op: any[]; substats?: Array<{ type?: string; value?: number }>;
p: number; ingameEquippedId?: string;
s: string; p?: string | number;
sk: number;
} }
const DatabasePage: React.FC = () => { const DatabasePage: React.FC = () => {
@@ -29,8 +28,123 @@ const DatabasePage: React.FC = () => {
const [selectedSessionId, setSelectedSessionId] = useState<number | null>(null); const [selectedSessionId, setSelectedSessionId] = useState<number | null>(null);
const [renameOpen, setRenameOpen] = useState(false); const [renameOpen, setRenameOpen] = useState(false);
const [renameValue, setRenameValue] = useState(''); 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 { 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 formatSessionLabel = (session: model.ParsedSession) => {
const date = session.created_at ? new Date(session.created_at * 1000) : null; const date = session.created_at ? new Date(session.created_at * 1000) : null;
const timeText = date ? date.toLocaleString() : ''; const timeText = date ? date.toLocaleString() : '';
@@ -88,9 +202,33 @@ const DatabasePage: React.FC = () => {
loadSessions(); 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 = () => { const openRename = () => {
if (!selectedSessionId) { if (!selectedSessionId) {
info('请先选择一条解析数据'); info('请先选择一条装备记录');
return; return;
} }
const current = sessions.find(s => s.id === selectedSessionId); const current = sessions.find(s => s.id === selectedSessionId);
@@ -124,7 +262,7 @@ const DatabasePage: React.FC = () => {
const handleDelete = async () => { const handleDelete = async () => {
if (!selectedSessionId) { if (!selectedSessionId) {
info('请先选择一条解析数据'); info('请先选择一条装备记录');
return; return;
} }
Modal.confirm({ Modal.confirm({
@@ -147,7 +285,7 @@ const DatabasePage: React.FC = () => {
} finally { } finally {
setLoading(false); setLoading(false);
} }
} },
}); });
}; };
@@ -158,67 +296,46 @@ const DatabasePage: React.FC = () => {
// 装备表格列定义 // 装备表格列定义
const equipmentColumns = [ const equipmentColumns = [
{ {
title: 'ID', title: '套装类型',
dataIndex: 'id', dataIndex: 'set',
key: 'id', key: 'set',
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, width: 120,
render: (_: any, record: Equipment) => SET_LABELS[record.set || ''] || record.set || '-',
}, },
{ {
title: '等级', title: '等级',
dataIndex: 'g', dataIndex: 'level',
key: 'g', key: 'level',
width: 80, width: 80,
render: (grade: number) => ( render: (value: number) => (value ?? '-'),
<Tag color={grade >= 5 ? 'red' : grade >= 3 ? 'orange' : 'green'}>
{grade}
</Tag>
),
}, },
{ {
title: '经验', title: '强化等级',
dataIndex: 'e', dataIndex: 'enhance',
key: 'e', key: 'enhance',
width: 90,
render: (value: number) => (value === undefined || value === null ? '-' : `+${value}`),
},
{
title: '部位',
dataIndex: 'gear',
key: 'gear',
width: 100, width: 100,
render: (exp: number) => exp.toLocaleString(), render: (gear: string) => GEAR_LABELS[gear] || gear || '-',
}, },
{ {
title: '力量', title: '主属性',
dataIndex: 'p', dataIndex: 'main',
key: 'p', key: 'main',
width: 80, width: 180,
render: (_: any, record: Equipment) => formatStat(record.main),
}, },
{ {
title: '魔法', title: '副属性',
dataIndex: 'mg', dataIndex: 'substats',
key: 'mg', key: 'substats',
width: 80, width: 320,
}, render: (_: any, record: Equipment) => formatSubstats(record.substats),
{
title: '技能',
dataIndex: 'sk',
key: 'sk',
width: 80,
},
{
title: '状态',
dataIndex: 'l',
key: 'l',
width: 80,
render: (locked: boolean) => (
<Tag color={locked ? 'red' : 'green'}>
{locked ? '锁定' : '正常'}
</Tag>
),
}, },
]; ];
@@ -268,6 +385,7 @@ const DatabasePage: React.FC = () => {
> >
</Button> </Button>
<Button icon={<DownloadOutlined />} onClick={exportData} disabled={!latestData?.geartxt}></Button>
<Select <Select
style={{ minWidth: 320 }} style={{ minWidth: 320 }}
placeholder="请选择解析数据" placeholder="请选择解析数据"
@@ -287,11 +405,57 @@ const DatabasePage: React.FC = () => {
</Card> </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 ? ( {latestData?.items && latestData.items.length > 0 ? (
<Table <Table
columns={equipmentColumns} columns={equipmentColumns}
dataSource={latestData.items} dataSource={filteredItems}
rowKey="id" rowKey="id"
loading={loading} loading={loading}
pagination={{ pagination={{
@@ -332,3 +496,4 @@ const DatabasePage: React.FC = () => {
}; };
export default DatabasePage; export default DatabasePage;

View File

@@ -321,6 +321,7 @@ export namespace model {
export class ParsedResult { export class ParsedResult {
items: any[]; items: any[];
heroes: any[]; heroes: any[];
geartxt: string;
static createFrom(source: any = {}) { static createFrom(source: any = {}) {
return new ParsedResult(source); return new ParsedResult(source);
@@ -330,6 +331,7 @@ export namespace model {
if ('string' === typeof source) source = JSON.parse(source); if ('string' === typeof source) source = JSON.parse(source);
this.items = source["items"]; this.items = source["items"];
this.heroes = source["heroes"]; this.heroes = source["heroes"];
this.geartxt = source["geartxt"];
} }
} }
export class ParsedSession { export class ParsedSession {

View File

@@ -30,13 +30,15 @@ export function GetParsedSessions():Promise<Array<model.ParsedSession>>;
export function OptimizeBuilds(arg1:model.OptimizeRequest):Promise<model.OptimizeResponse>; 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 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 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>; export function StartCapture(arg1:string):Promise<void>;

View File

@@ -58,6 +58,10 @@ export function OptimizeBuilds(arg1) {
return window['go']['service']['App']['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) { export function ParseData(arg1) {
return window['go']['service']['App']['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); return window['go']['service']['App']['SaveAppSetting'](arg1, arg2);
} }
export function SaveParsedDataToDatabase(arg1, arg2, arg3) { export function SaveParsedDataToDatabase(arg1, arg2, arg3, arg4) {
return window['go']['service']['App']['SaveParsedDataToDatabase'](arg1, arg2, arg3); return window['go']['service']['App']['SaveParsedDataToDatabase'](arg1, arg2, arg3, arg4);
} }
export function StartCapture(arg1) { export function StartCapture(arg1) {

1
gear.txt Normal file

File diff suppressed because one or more lines are too long

19
go.mod
View File

@@ -1,6 +1,6 @@
module equipment-analyzer module equipment-analyzer
go 1.22.0 go 1.24.0
toolchain go1.24.4 toolchain go1.24.4
@@ -9,7 +9,7 @@ require (
github.com/wailsapp/wails/v2 v2.11.0 github.com/wailsapp/wails/v2 v2.11.0
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
modernc.org/sqlite v1.29.0 modernc.org/sqlite v1.45.0
) )
require ( require (
@@ -19,7 +19,6 @@ require (
github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // 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/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/labstack/echo/v4 v4.13.3 // indirect github.com/labstack/echo/v4 v4.13.3 // indirect
github.com/labstack/gommon v0.4.2 // indirect github.com/labstack/gommon v0.4.2 // indirect
@@ -29,7 +28,7 @@ require (
github.com/leaanthony/u v1.1.1 // indirect github.com/leaanthony/u v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // 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/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
@@ -42,13 +41,11 @@ require (
github.com/wailsapp/mimetype v1.4.1 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect
go.uber.org/multierr v1.10.0 // indirect go.uber.org/multierr v1.10.0 // indirect
golang.org/x/crypto v0.33.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/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 golang.org/x/text v0.22.0 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect modernc.org/libc v1.67.6 // indirect
modernc.org/libc v1.41.0 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.11.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
) )

66
go.sum
View File

@@ -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/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 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 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-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= 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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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= 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.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.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
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/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=
@@ -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.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 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= 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/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.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 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-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-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.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 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 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.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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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-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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/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.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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.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.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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/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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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/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-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.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= 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= 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 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk= modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY= modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/sqlite v1.29.0 h1:lQVw+ZsFM3aRG5m4myG70tbXpr3S/J1ej0KHIP4EvjM= modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/sqlite v1.29.0/go.mod h1:hG41jCYxOAOoO6BRK66AdRlmOcDzXf7qnwlwjUIOqa0= modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= 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 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

View File

@@ -6,59 +6,60 @@ import (
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"time" "time"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
) )
// Database 数据库管理器 // Database manages the app database.
type Database struct { type Database struct {
db *sql.DB db *sql.DB
} }
// NewDatabase 创建新的数据库连接 // NewDatabase creates a new database connection.
func NewDatabase() (*Database, error) { func NewDatabase() (*Database, error) {
dbPath := getDatabasePath() dbPath := getDatabasePath()
log.Printf("[db] init: path=%s", dbPath) log.Printf("[db] init: path=%s", dbPath)
// 确保目录存在 // Ensure directory exists.
dir := filepath.Dir(dbPath) dir := filepath.Dir(dbPath)
if err := os.MkdirAll(dir, 0755); err != nil { if err := os.MkdirAll(dir, 0755); err != nil {
log.Printf("[db] mkdir failed: dir=%s err=%v", dir, err) 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) db, err := sql.Open("sqlite", dbPath)
if err != nil { if err != nil {
log.Printf("[db] open failed: path=%s err=%v", dbPath, err) 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 { if err := db.Ping(); err != nil {
log.Printf("[db] ping failed: err=%v", err) 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} database := &Database{db: db}
// 初始化表结构 // Init tables.
if err := database.initTables(); err != nil { if err := database.initTables(); err != nil {
log.Printf("[db] init tables failed: err=%v", err) 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") log.Printf("[db] init ok")
return database, nil return database, nil
} }
// Close 关闭数据库连接 // Close closes the database connection.
func (d *Database) Close() error { func (d *Database) Close() error {
return d.db.Close() return d.db.Close()
} }
// getDatabasePath 获取数据库文件路径 // getDatabasePath returns the database file path.
func getDatabasePath() string { func getDatabasePath() string {
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { if err != nil {
@@ -67,19 +68,18 @@ func getDatabasePath() string {
return filepath.Join(homeDir, ".equipment-analyzer", "equipment_analyzer.db") return filepath.Join(homeDir, ".equipment-analyzer", "equipment_analyzer.db")
} }
// initTables 初始化数据库表结构 // initTables creates tables if not exist.
func (d *Database) initTables() error { func (d *Database) initTables() error {
// 解析数据表 - 存储抓包解析后的装备和角色数据
parsedDataTable := ` parsedDataTable := `
CREATE TABLE IF NOT EXISTS parsed_data ( CREATE TABLE IF NOT EXISTS parsed_data (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
session_name TEXT NOT NULL, session_name TEXT NOT NULL,
items_json TEXT NOT NULL, items_json TEXT NOT NULL,
heroes_json TEXT NOT NULL, heroes_json TEXT NOT NULL,
geartxt TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL created_at INTEGER NOT NULL
);` );`
// 应用设置表
settingsTable := ` settingsTable := `
CREATE TABLE IF NOT EXISTS app_settings ( CREATE TABLE IF NOT EXISTS app_settings (
key TEXT PRIMARY KEY, key TEXT PRIMARY KEY,
@@ -94,44 +94,59 @@ func (d *Database) initTables() error {
for _, table := range tables { for _, table := range tables {
if _, err := d.db.Exec(table); err != nil { 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 return nil
} }
// SaveParsedData 保存解析后的数据 func (d *Database) ensureParsedDataColumns() error {
func (d *Database) SaveParsedData(sessionName string, itemsJSON, heroesJSON string) error { _, err := d.db.Exec("ALTER TABLE parsed_data ADD COLUMN geartxt TEXT NOT NULL DEFAULT ''")
stmt := ` if err != nil {
INSERT INTO parsed_data (session_name, items_json, heroes_json, created_at) if strings.Contains(err.Error(), "duplicate column name") {
VALUES (?, ?, ?, ?)` 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 return err
} }
// GetLatestParsedData 获取最新的解析数据 // GetLatestParsedData returns latest parsed data.
func (d *Database) GetLatestParsedData() (string, string, error) { func (d *Database) GetLatestParsedData() (string, string, string, error) {
stmt := ` stmt := `
SELECT items_json, heroes_json SELECT items_json, heroes_json, geartxt
FROM parsed_data FROM parsed_data
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT 1` LIMIT 1`
var itemsJSON, heroesJSON string var itemsJSON, heroesJSON, gearTxt string
err := d.db.QueryRow(stmt).Scan(&itemsJSON, &heroesJSON) err := d.db.QueryRow(stmt).Scan(&itemsJSON, &heroesJSON, &gearTxt)
if err != nil { if err != nil {
if err == sql.ErrNoRows { 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) { func (d *Database) GetParsedSessions() ([]ParsedSession, error) {
stmt := ` stmt := `
SELECT id, session_name, created_at SELECT id, session_name, created_at
@@ -155,26 +170,26 @@ func (d *Database) GetParsedSessions() ([]ParsedSession, error) {
return sessions, nil return sessions, nil
} }
// GetParsedDataByID 获取指定会话的数据 // GetParsedDataByID returns parsed data for a session.
func (d *Database) GetParsedDataByID(id int64) (string, string, error) { func (d *Database) GetParsedDataByID(id int64) (string, string, string, error) {
stmt := ` stmt := `
SELECT items_json, heroes_json SELECT items_json, heroes_json, geartxt
FROM parsed_data FROM parsed_data
WHERE id = ? WHERE id = ?
LIMIT 1` LIMIT 1`
var itemsJSON, heroesJSON string var itemsJSON, heroesJSON, gearTxt string
err := d.db.QueryRow(stmt, id).Scan(&itemsJSON, &heroesJSON) err := d.db.QueryRow(stmt, id).Scan(&itemsJSON, &heroesJSON, &gearTxt)
if err != nil { if err != nil {
if err == sql.ErrNoRows { 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 { func (d *Database) UpdateParsedSessionName(id int64, name string) error {
stmt := ` stmt := `
UPDATE parsed_data UPDATE parsed_data
@@ -184,7 +199,7 @@ func (d *Database) UpdateParsedSessionName(id int64, name string) error {
return err return err
} }
// DeleteParsedSession 删除解析会话 // DeleteParsedSession deletes a parsed session.
func (d *Database) DeleteParsedSession(id int64) error { func (d *Database) DeleteParsedSession(id int64) error {
stmt := ` stmt := `
DELETE FROM parsed_data DELETE FROM parsed_data
@@ -193,14 +208,14 @@ func (d *Database) DeleteParsedSession(id int64) error {
return err return err
} }
// SaveSetting 保存应用设置 // SaveSetting saves app setting.
func (d *Database) SaveSetting(key, value string) error { func (d *Database) SaveSetting(key, value string) error {
stmt := "INSERT OR REPLACE INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)" stmt := "INSERT OR REPLACE INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)"
_, err := d.db.Exec(stmt, key, value, time.Now().Unix()) _, err := d.db.Exec(stmt, key, value, time.Now().Unix())
return err return err
} }
// GetSetting 获取应用设置 // GetSetting returns app setting.
func (d *Database) GetSetting(key string) (string, error) { func (d *Database) GetSetting(key string) (string, error) {
stmt := "SELECT value FROM app_settings WHERE key = ?" stmt := "SELECT value FROM app_settings WHERE key = ?"
var value string var value string
@@ -211,7 +226,7 @@ func (d *Database) GetSetting(key string) (string, error) {
return value, nil return value, nil
} }
// GetAllSettings 获取所有设置 // GetAllSettings returns all settings.
func (d *Database) GetAllSettings() (map[string]string, error) { func (d *Database) GetAllSettings() (map[string]string, error) {
stmt := "SELECT key, value FROM app_settings" stmt := "SELECT key, value FROM app_settings"
rows, err := d.db.Query(stmt) rows, err := d.db.Query(stmt)

View File

@@ -34,6 +34,7 @@ type CaptureStatus struct {
type ParsedResult struct { type ParsedResult struct {
Items []interface{} `json:"items"` Items []interface{} `json:"items"`
Heroes []interface{} `json:"heroes"` Heroes []interface{} `json:"heroes"`
GearTxt string `json:"geartxt"`
} }
// ParsedSession 解析数据会话信息 // ParsedSession 解析数据会话信息

View File

@@ -22,8 +22,8 @@ func NewDatabaseService(db *model.Database, logger *utils.Logger) *DatabaseServi
} }
// SaveParsedDataToDatabase 保存解析后的数据到数据库 // SaveParsedDataToDatabase 保存解析后的数据到数据库
func (s *DatabaseService) SaveParsedDataToDatabase(sessionName string, itemsJSON, heroesJSON string) error { func (s *DatabaseService) SaveParsedDataToDatabase(sessionName string, itemsJSON, heroesJSON, gearTxt string) error {
err := s.db.SaveParsedData(sessionName, itemsJSON, heroesJSON) err := s.db.SaveParsedData(sessionName, itemsJSON, heroesJSON, gearTxt)
if err != nil { if err != nil {
s.logger.Error("保存解析数据到数据库失败", "error", err, "session_name", sessionName) s.logger.Error("保存解析数据到数据库失败", "error", err, "session_name", sessionName)
return fmt.Errorf("保存解析数据失败: %w", err) return fmt.Errorf("保存解析数据失败: %w", err)
@@ -34,15 +34,15 @@ func (s *DatabaseService) SaveParsedDataToDatabase(sessionName string, itemsJSON
} }
// GetLatestParsedDataFromDatabase 从数据库获取最新的解析数据 // GetLatestParsedDataFromDatabase 从数据库获取最新的解析数据
func (s *DatabaseService) GetLatestParsedDataFromDatabase() (string, string, error) { func (s *DatabaseService) GetLatestParsedDataFromDatabase() (string, string, string, error) {
itemsJSON, heroesJSON, err := s.db.GetLatestParsedData() itemsJSON, heroesJSON, gearTxt, err := s.db.GetLatestParsedData()
if err != nil { if err != nil {
s.logger.Error("从数据库获取最新解析数据失败", "error", err) s.logger.Error("从数据库获取最新解析数据失败", "error", err)
return "", "", fmt.Errorf("获取解析数据失败: %w", err) return "", "", "", fmt.Errorf("获取解析数据失败: %w", err)
} }
s.logger.Info("最新解析数据获取成功") s.logger.Info("最新解析数据获取成功")
return itemsJSON, heroesJSON, nil return itemsJSON, heroesJSON, gearTxt, nil
} }
// GetParsedSessions 从数据库获取所有解析会话 // GetParsedSessions 从数据库获取所有解析会话
@@ -56,13 +56,13 @@ func (s *DatabaseService) GetParsedSessions() ([]model.ParsedSession, error) {
} }
// GetParsedDataByID 从数据库获取指定会话数据 // GetParsedDataByID 从数据库获取指定会话数据
func (s *DatabaseService) GetParsedDataByID(id int64) (string, string, error) { func (s *DatabaseService) GetParsedDataByID(id int64) (string, string, string, error) {
itemsJSON, heroesJSON, err := s.db.GetParsedDataByID(id) itemsJSON, heroesJSON, gearTxt, err := s.db.GetParsedDataByID(id)
if err != nil { if err != nil {
s.logger.Error("从数据库获取解析数据失败", "error", err, "id", id) 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 更新解析会话名称 // UpdateParsedSessionName 更新解析会话名称

View File

@@ -365,7 +365,7 @@ func (a *App) StopAndParseCapture() (*model.ParsedResult, error) {
} }
sessionName := fmt.Sprintf("capture_%d", time.Now().Unix()) 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) a.logger.Error("save parsed data failed", "error", err)
} else { } else {
a.logger.Info("parsed data saved", "session_name", sessionName) a.logger.Info("parsed data saved", "session_name", sessionName)
@@ -376,11 +376,42 @@ func (a *App) StopAndParseCapture() (*model.ParsedResult, error) {
} }
// SaveParsedDataToDatabase saves parsed data. // 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 { if a.databaseService == nil {
return fmt.Errorf("database service not initialized") 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. // 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") return nil, fmt.Errorf("database service not initialized")
} }
itemsJSON, heroesJSON, err := a.databaseService.GetLatestParsedDataFromDatabase() itemsJSON, heroesJSON, gearTxt, err := a.databaseService.GetLatestParsedDataFromDatabase()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -412,6 +443,7 @@ func (a *App) GetLatestParsedDataFromDatabase() (*model.ParsedResult, error) {
return &model.ParsedResult{ return &model.ParsedResult{
Items: items, Items: items,
Heroes: heroes, Heroes: heroes,
GearTxt: gearTxt,
}, nil }, nil
} }
@@ -428,7 +460,7 @@ func (a *App) GetParsedDataByID(id int64) (*model.ParsedResult, error) {
if a.databaseService == nil { if a.databaseService == nil {
return nil, fmt.Errorf("database service not initialized") 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 { if err != nil {
return nil, err return nil, err
} }
@@ -450,6 +482,7 @@ func (a *App) GetParsedDataByID(id int64) (*model.ParsedResult, error) {
return &model.ParsedResult{ return &model.ParsedResult{
Items: items, Items: items,
Heroes: heroes, Heroes: heroes,
GearTxt: gearTxt,
}, nil }, nil
} }

View File

@@ -26,6 +26,7 @@ type ParserService struct {
heroBase map[string]heroBaseStats heroBase map[string]heroBaseStats
heroOnce sync.Once heroOnce sync.Once
heroErr error heroErr error
fallbackMainStatCount int
} }
func NewParserService(cfg *config.Config, logger *utils.Logger) *ParserService { 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 解析十六进制数据 // ParseHexData 解析十六进制数据
func (ps *ParserService) ParseHexData(hexDataList []string) (*model.ParsedResult, string, error) { func (ps *ParserService) ParseHexData(hexDataList []string) (*model.ParsedResult, string, error) {
if len(hexDataList) == 0 { if len(hexDataList) == 0 {
ps.logger.Warn("没有数据需要解析") ps.logger.Warn("no data to parse")
return &model.ParsedResult{ return &model.ParsedResult{
Items: make([]interface{}, 0), Items: make([]interface{}, 0),
Heroes: make([]interface{}, 0), Heroes: make([]interface{}, 0),
}, "", nil }, "", 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" url := "https://krivpfvxi0.execute-api.us-west-2.amazonaws.com/dev/getItems"
reqBody := map[string]interface{}{ reqBody := map[string]interface{}{
"data": hexDataList, "data": hexDataList,
} }
jsonBytes, _ := json.Marshal(reqBody) 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)) req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBytes))
if err == nil { if err == nil {
req.Header.Set("Content-Type", "application/json") 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 { if err == nil && resp.StatusCode == 200 {
defer resp.Body.Close() defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body) body, _ := ioutil.ReadAll(resp.Body)
ps.writeParseSnapshot("response", body)
// 新校验逻辑校验data和units字段
var raw map[string]interface{} var raw map[string]interface{}
if err := json.Unmarshal(body, &raw); err != nil { if err := json.Unmarshal(body, &raw); err != nil {
ps.logger.Error("远程json解析失败", "error", err) ps.logger.Error("remote json unmarshal failed", "error", err)
return nil, "", fmt.Errorf("远程json解析失败: %v", err) return nil, "", fmt.Errorf("remote json unmarshal failed: %v", err)
} }
// 校验data字段
dataArr, dataOk := raw["data"].([]interface{}) dataArr, dataOk := raw["data"].([]interface{})
if !dataOk || len(dataArr) == 0 { if !dataOk || len(dataArr) == 0 {
ps.logger.Error("远程json校验失败data字段缺失或为空") ps.logger.Error("remote json validate failed: data missing or empty")
return nil, "", fmt.Errorf("远程json校验失败data字段缺失或为空") return nil, "", fmt.Errorf("remote json validate failed: data missing or empty")
} }
// 校验通过,直接解析数据 ps.logger.Info("remote json validate ok, start parse")
ps.logger.Info("远程原始数据校验通过,开始解析")
parsedResult, err := ps.ReadRawJsonFile(string(body)) parsedResult, err := ps.ReadRawJsonFile(string(body))
if err != nil { if err != nil {
return nil, "", err return nil, "", err
@@ -86,15 +104,15 @@ func (ps *ParserService) ParseHexData(hexDataList []string) (*model.ParsedResult
return parsedResult, "", nil return parsedResult, "", nil
} else if err != nil { } else if err != nil {
ps.logger.Error("远程解析请求失败", "error", err) ps.logger.Error("remote parse request failed", "error", err)
return nil, "", fmt.Errorf("远程解析请求失败: %v", err) return nil, "", fmt.Errorf("remote parse request failed: %v", err)
} else { } else {
ps.logger.Error("远程解析响应码异常", "status", resp.StatusCode) ps.logger.Error("remote parse http status", "status", resp.StatusCode)
return nil, "", fmt.Errorf("远程解析响应码异常: %d", resp.StatusCode) return nil, "", fmt.Errorf("remote parse http status %d", resp.StatusCode)
} }
} else { } else {
ps.logger.Error("远程解析请求构建失败", "error", err) ps.logger.Error("remote parse request build failed", "error", err)
return nil, "", fmt.Errorf("远程解析请求构建失败: %v", 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 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{}) equips, _ := rawData["data"].([]interface{})
// 修正units 取最大长度的那组 // 修正units 取最大长度的那组
var rawUnits []interface{} var rawUnits []interface{}
@@ -131,8 +204,10 @@ func (ps *ParserService) ReadRawJsonFile(rawJson string) (*model.ParsedResult, e
} }
// 转换装备数据 // 转换装备数据
ps.fallbackMainStatCount = 0
convertedItems := ps.convertItemsAllWithLog(validEquips) convertedItems := ps.convertItemsAllWithLog(validEquips)
// 转换英雄数据(只对最大组) ps.logger.Info("main stat fallback count", "count", ps.fallbackMainStatCount)
// 转换英雄数据(只对最大组?
convertedHeroes := ps.convertUnits(rawUnits) convertedHeroes := ps.convertUnits(rawUnits)
result := &model.ParsedResult{ result := &model.ParsedResult{
@@ -145,10 +220,115 @@ func (ps *ParserService) ReadRawJsonFile(rawJson string) (*model.ParsedResult, e
for i, v := range convertedHeroes { for i, v := range convertedHeroes {
result.Heroes[i] = v result.Heroes[i] = v
} }
result.GearTxt = rawJson
return result, nil 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 转换装备数据 // convertItems 转换装备数据
func (ps *ParserService) convertItems(rawItems []interface{}) []map[string]interface{} { func (ps *ParserService) convertItems(rawItems []interface{}) []map[string]interface{} {
var convertedItems []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.convertEnhance(converted)
// 转换主属 // 转换主属?
ps.convertMainStat(converted) ps.convertMainStat(converted)
// 转换副属 // 转换副属?
ps.convertSubStats(converted) ps.convertSubStats(converted)
// 转换ID // 转换ID
@@ -227,7 +407,7 @@ func (ps *ParserService) convertUnits(rawUnits []interface{}) []map[string]inter
convertedUnit[key] = value convertedUnit[key] = value
} }
// 转换星星和觉 // 转换星星和觉?
if g, exists := unitMap["g"]; exists { if g, exists := unitMap["g"]; exists {
convertedUnit["stars"] = g convertedUnit["stars"] = g
} }
@@ -335,6 +515,7 @@ func (ps *ParserService) convertEnhance(item map[string]interface{}) {
func (ps *ParserService) convertMainStat(item map[string]interface{}) { func (ps *ParserService) convertMainStat(item map[string]interface{}) {
op, opExists := item["op"].([]interface{}) op, opExists := item["op"].([]interface{})
mainStatValue, mainStatExists := item["mainStatValue"].(float64) mainStatValue, mainStatExists := item["mainStatValue"].(float64)
fallbackUsed := false
if opExists && len(op) > 0 && mainStatExists { if opExists && len(op) > 0 && mainStatExists {
if mainOp, ok := op[0].([]interface{}); ok && len(mainOp) > 0 { 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{}) { 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{}) statAcc := make(map[string]map[string]interface{})
// 处理副属(从索引1开始) // 处理副属?(从索?开?
for i := 1; i < len(op); i++ { for i := 1; i < len(op); i++ {
if opItem, ok := op[i].([]interface{}); ok && len(opItem) >= 2 { if opItem, ok := op[i].([]interface{}); ok && len(opItem) >= 2 {
opType, _ := opItem[0].(string) opType, _ := opItem[0].(string)
@@ -417,7 +631,7 @@ func (ps *ParserService) convertSubStats(item map[string]interface{}) {
} }
} }
// 转换为最终格 // 转换为最终格?
var substats []interface{} var substats []interface{}
for statType, statData := range statAcc { for statType, statData := range statAcc {
substat := map[string]interface{}{ substat := map[string]interface{}{
@@ -765,3 +979,4 @@ func (ps *ParserService) applyHeroBase(unit map[string]interface{}, base heroBas
unit["res"] = base.Res unit["res"] = base.Res
} }
} }