重构UIGF导入导出备份恢复,支持UIGF4.2

This commit is contained in:
BTMuli
2026-02-07 21:24:35 +08:00
parent b5562a0fce
commit 9be8c78deb
10 changed files with 549 additions and 298 deletions

View File

@@ -1,13 +1,14 @@
/**
* UIGF工具类
* @since Beta v0.9.2
* @since Beta v0.9.5
*/
import showLoading from "@comp/func/loading.js";
import showSnackbar from "@comp/func/snackbar.js";
import TSUserGacha from "@Sqlm/userGacha.js";
import TSUserGachaB from "@Sqlm/userGachaB.js";
import { app, path } from "@tauri-apps/api";
import { readTextFile, writeTextFile } from "@tauri-apps/plugin-fs";
import { exists, mkdir, readDir, readTextFile, writeTextFile } from "@tauri-apps/plugin-fs";
import { Ajv } from "ajv";
import type { ErrorObject } from "ajv/lib/types/index.js";
@@ -64,9 +65,9 @@ async function getUigfHeader(uid: string, timezone: number): Promise<TGApp.Plugi
}
/**
* 获取 UIGF v4.1 头部信息
* @since Beta v0.7.10
* @returns UIGF v4.1 头部信息
* 获取 UIGF v4.2 头部信息
* @since Beta v0.9.5
* @returns UIGF v4.2 头部信息
*/
export async function getUigf4Header(): Promise<TGApp.Plugins.UIGF.Info4> {
const stamp = Date.now();
@@ -74,40 +75,62 @@ export async function getUigf4Header(): Promise<TGApp.Plugins.UIGF.Info4> {
export_timestamp: Math.floor(stamp / 1000).toString(),
export_app: "TeyvatGuide",
export_app_version: await app.getVersion(),
version: "v4.1",
version: "v4.2",
lang: "zh-cn",
};
}
/**
* 数据转换-数据库到 UIGF
* @since Beta v0.7.5
* @param data - 数据库数据
* 数据库祈愿数据转换为UIGF祈愿数据
* @since Beta v0.9.5
* @param data - 数据库祈愿数据
* @param timezone - 时区
* @returns UIGF 数据
* @returns UIGF 祈愿数据
*/
function convertDataToUigf(
function convertHk4e2Uigf(
data: Array<TGApp.Sqlite.Gacha.Gacha>,
timezone: number,
): Array<TGApp.Plugins.UIGF.GachaItem> {
return data.map((gacha) => {
return {
gacha_type: gacha.gachaType,
item_id: gacha.itemId,
count: gacha.count,
time: getExportTime(gacha.time, timezone),
name: gacha.name,
item_type: gacha.type,
rank_type: gacha.rank,
id: gacha.id,
uigf_gacha_type: gacha.uigfType,
};
});
return data.map((gacha) => ({
gacha_type: gacha.gachaType,
item_id: gacha.itemId,
count: gacha.count,
time: getExportTime(gacha.time, timezone),
name: gacha.name,
item_type: gacha.type,
rank_type: gacha.rank,
id: gacha.id,
uigf_gacha_type: gacha.uigfType,
}));
}
/**
* 数据库颂愿数据转换为UIGF颂愿数据
* @since Beta v0.9.5
* @param data - 数据库颂愿数据
* @param timezone - 时区
* @returns UIGF 颂愿数据
*/
function convertUgc2Uigf(
data: Array<TGApp.Sqlite.Gacha.GachaB>,
timezone: number,
): Array<TGApp.Plugins.UIGF.GachaItemB> {
return data.map((gacha) => ({
id: gacha.id,
schedule_id: gacha.scheduleId,
item_type: gacha.type,
item_id: gacha.itemId,
item_name: gacha.name,
rank_type: gacha.rank,
time: getExportTime(gacha.time, timezone),
op_gacha_type: gacha.opGachaType,
}));
}
/**
* 检测是否是v4版本的UIGF
* @since Beta v0.9.2
* @since Beta v0.9.5
* @remarks 祈愿&颂愿数据需要进一步确定
* @param path - UIGF 文件路径
* @returns 是否是v4null表示数据异常
*/
@@ -180,7 +203,7 @@ function validateUigfData(data: object): boolean {
}
/**
* 验证 UIGF v4.0 数据
* 验证 UIGF v4.2 数据
* @since Beta v0.5.0
* @param data - UIGF 数据
* @returns 是否验证通过
@@ -197,29 +220,31 @@ function validateUigf4Data(data: object): boolean {
/**
* 读取 UIGF 数据
* @since Beta v0.5.0
* @since Beta v0.9.5
* @todo 重构
* @param userPath - UIGF 数据路径
* @returns UIGF 数据
*/
export async function readUigfData(userPath: string): Promise<TGApp.Plugins.UIGF.Schema> {
const fileData: string = await readTextFile(userPath);
return JSON.parse(fileData) satisfies TGApp.Plugins.UIGF.Schema;
return <TGApp.Plugins.UIGF.Schema>JSON.parse(fileData);
}
/**
* 读取 UIGF 4.0 数据
* @since Beta v0.5.0
* @since Beta v0.9.5
* @todo 重构
* @param userPath - UIGF 数据路径
* @returns UIGF 数据
*/
export async function readUigf4Data(userPath: string): Promise<TGApp.Plugins.UIGF.Schema4> {
const fileData: string = await readTextFile(userPath);
return JSON.parse(fileData) satisfies TGApp.Plugins.UIGF.Schema4;
return <TGApp.Plugins.UIGF.Schema4>JSON.parse(fileData);
}
/**
* 导出 UIGF 数据
* @since Beta v0.7.5
* @since Beta v0.9.5
* @param uid - UID
* @param gachaList - 祈愿列表
* @param savePath - 保存路径
@@ -233,29 +258,150 @@ export async function exportUigfData(
const timezone = getUigfTimeZone(uid);
const UigfData = {
info: await getUigfHeader(uid, timezone),
list: convertDataToUigf(gachaList, timezone),
list: convertHk4e2Uigf(gachaList, timezone),
};
const filePath = savePath ?? `${await path.appLocalDataDir()}userData\\UIGF_${uid}.json`;
const filePath =
savePath ?? `${await path.appLocalDataDir()}userData${path.sep()}UIGF_${uid}.json`;
await writeTextFile(filePath, JSON.stringify(UigfData));
}
/**
* 导出UIGF4数据
* @since Beta v0.9.1
* @param uids - UID列表
* @since Beta v0.9.5
* @param usg - 祈愿UID列表
* @param usb - 颂愿UID列表
* @param savePath - 保存路径
* @returns
*/
export async function exportUigf4Data(uids: Array<string> = [], savePath?: string): Promise<void> {
export async function exportUigf4Data(
usg: Array<string> = [],
usb: Array<string> = [],
savePath?: string,
): Promise<void> {
const header = await getUigf4Header();
const filePath = savePath ?? `${await path.appLocalDataDir()}userData\\UIGF4.json`;
const data: Array<TGApp.Plugins.UIGF.GachaHk4e> = [];
for (const uid of uids) {
const filePath = savePath ?? `${await path.appLocalDataDir()}userData${path.sep()}UIGF4.json`;
const dataHk4e: Array<TGApp.Plugins.UIGF.GachaHk4e> = [];
const dataUgc: Array<TGApp.Plugins.UIGF.GachaUgc> = [];
for (const uid of usg) {
const gachaList = await TSUserGacha.record.all(uid);
await showLoading.update(`正在导出${uid}${gachaList.length}条祈愿记录`);
const timezone = getUigfTimeZone(uid);
data.push({ uid: uid, timezone: timezone, list: convertDataToUigf(gachaList, timezone) });
dataHk4e.push({ uid: uid, timezone: timezone, list: convertHk4e2Uigf(gachaList, timezone) });
}
const uigf4Data: TGApp.Plugins.UIGF.Schema4 = { info: header, hk4e: data };
for (const uid of usb) {
const gachaList = await TSUserGachaB.getGachaRecords(uid);
await showLoading.update(`正在导出${uid}${gachaList.length}条颂愿记录`);
const timezone = getUigfTimeZone(uid);
dataUgc.push({ uid: uid, timezone: timezone, list: convertUgc2Uigf(gachaList, timezone) });
}
const uigf4Data: TGApp.Plugins.UIGF.Schema4 = { info: header, hk4e: dataHk4e, hk4e_ugc: dataUgc };
await writeTextFile(filePath, JSON.stringify(uigf4Data, null, 2));
}
/**
* 备份祈愿数据
* @since Beta v0.9.5
* @param dir - 备份目录
* @returns 无返回值
*/
export async function backUpUigf(dir: string): Promise<void> {
if (!(await exists(dir))) {
await TGLogger.Warn("不存在指定的祈愿备份目录,即将创建");
await mkdir(dir, { recursive: true });
}
const usg = await TSUserGacha.getUidList();
const usb = await TSUserGachaB.getUidList();
// TODO: 调整备份文件名称
const savePath = `${dir}${path.sep()}UIGF4.json`;
await exportUigf4Data(usg, usb, savePath);
await TGLogger.Info("祈愿数据备份完成");
}
/**
* 恢复祈愿数据-旧版
* @since Beta v0.9.5
* @param dir - 目录
* @param file - 文件名称
* @returns 是否恢复成功
*/
async function restoreUigfByLegacyFile(dir: string, file: string): Promise<boolean> {
await showLoading.update(`祈愿文件: ${file}`);
const filePath = `${dir}${path.sep()}${file}`;
try {
const check = await verifyUigfData(filePath);
if (!check) {
await showLoading.update(`UIGF数据校验失败`);
await TGLogger.Warn(`UIGF数据校验失败${filePath}`);
return false;
}
const data = await readUigfData(filePath);
const uid = data.info.uid;
await showLoading.update(`正在导入${data.info.uid}${data.list.length}条祈愿数据`);
await TSUserGacha.mergeUIGF(uid, data.list, true);
} catch (e) {
await TGLogger.Error(`恢复祈愿数据失败${dir}`);
await TGLogger.Error(typeof e === "string" ? e : JSON.stringify(e));
return false;
}
return true;
}
/**
* 恢复祈愿数据
* @since Beta v0.9.5
* @param filePath - 文件路径
* @returns 是否恢复成功
*/
async function restoreUigfByFile(filePath: string): Promise<boolean> {
try {
const check = await verifyUigfData(filePath, true);
if (!check) {
await TGLogger.Warn(`UIGF数据校验失败${filePath}`);
return false;
}
const read = await readUigf4Data(filePath);
for (const data of read?.hk4e ?? []) {
await showLoading.update(`正在导入${data.uid}${data.list.length}条祈愿记录`);
await TSUserGacha.mergeUIGF4(data, true);
}
for (const data of read?.hk4e_ugc ?? []) {
await showLoading.update(`正在导入${data.uid}${data.list.length}条颂愿记录`);
await TSUserGachaB.mergeUIGF4(data, true);
}
} catch (e) {
await TGLogger.Error(`恢复祈愿数据失败 ${filePath}`);
await TGLogger.Error(typeof e === "string" ? e : JSON.stringify(e));
return false;
}
return true;
}
/**
* 恢复祈愿数据
* @since Beta v0.9.5
* @param dir - 备份目录
* @returns 是否恢复成功
*/
export async function restoreUigf(dir: string): Promise<boolean> {
if (!(await exists(dir))) {
await TGLogger.Warn("不存在指定的祈愿备份目录");
return false;
}
let cnt = 0;
// 适配旧版备份文件 UIGF_{{uid}}.json
const legacyReg = /^UIGF_\d+\.json$/;
const legacyFiles = (await readDir(dir)).filter(
(item) => item.isFile && legacyReg.test(item.name),
);
for (const file of legacyFiles) {
const checkL = await restoreUigfByLegacyFile(dir, file.name);
if (checkL) cnt++;
}
const filePath = `${dir}${path.sep()}UIGF4.json`;
if (!(await exists(filePath))) {
await TGLogger.Warn(`未检测到UIGF4备份文件`);
} else {
await restoreUigfByFile(filePath);
}
return cnt > 0;
}

View File

@@ -1,7 +1,8 @@
/**
* 用户数据的备份、恢复、迁移
* @since Beta v0.9.0
* @since Beta v0.9.5
*/
import showLoading from "@comp/func/loading.js";
import showSnackbar from "@comp/func/snackbar.js";
import TSUserAbyss from "@Sqlm/userAbyss.js";
@@ -9,15 +10,15 @@ import TSUserAccount from "@Sqlm/userAccount.js";
import TSUserAchi from "@Sqlm/userAchi.js";
import TSUserChallenge from "@Sqlm/userChallenge.js";
import TSUserCombat from "@Sqlm/userCombat.js";
import TSUserGacha from "@Sqlm/userGacha.js";
import { exists, mkdir } from "@tauri-apps/plugin-fs";
import { backUpUigf, restoreUigf } from "@utils/UIGF.js";
import TGLogger from "./TGLogger.js";
/**
* 备份用户数据
* @since Beta v0.9.0
* @TODO 重构祈愿部分备份&读取,处理胡桃数据&应用自身数据的导入
* @since Beta v0.9.5
* @TODO 处理胡桃数据&应用自身数据的导入
* @param dir - 备份目录路径
* @returns 无返回值
*/
@@ -36,13 +37,13 @@ export async function backUpUserData(dir: string): Promise<void> {
await TSUserCombat.backupCombat(dir);
await showLoading.update("正在备份幽境危战数据");
await TSUserChallenge.backupChallenge(dir);
await showLoading.update("正在备份UIGF祈愿数据");
await TSUserGacha.backUpUigf(dir);
await showLoading.update("正在备份祈愿数据");
await backUpUigf(dir);
}
/**
* 恢复用户数据
* @since Beta v0.9.0
* @since Beta v0.9.5
* @param dir - 备份目录路径
* @returns 无返回值
*/
@@ -83,7 +84,7 @@ export async function restoreUserData(dir: string): Promise<void> {
errNum++;
}
await showLoading.update("正在恢复祈愿数据");
const restoreGacha = await TSUserGacha.restoreUigf(dir);
const restoreGacha = await restoreUigf(dir);
if (!restoreGacha) {
showSnackbar.error("祈愿数据恢复失败");
errNum++;

View File

@@ -1,6 +1,6 @@
/**
* 一些工具函数
* @since Beta v0.9.2
* @since Beta v0.9.5
*/
import { tz } from "@date-fns/tz";
@@ -391,3 +391,17 @@ export function validEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
/**
* 传入时间字符串跟对应时区转成utc8时间字符串
* @since Beta v0.9.5
* @param time - 时间字符串
* @param timezone - 时区
* @returns 转换后的时间戳
*/
export function getUtc8Time(time: string, timezone: number): string {
const date = new Date(time);
const diffTimezone = -timezone + 8;
const realDate = new Date(date.getTime() + diffTimezone * 60 * 60 * 1000);
return timestampToDate(realDate.getTime());
}