重构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,3 +1,4 @@
<!-- UIGF4导入导出组件 -->
<template>
<TOverlay v-model="visible" blur-val="5px">
<div class="ugo-box">
@@ -32,7 +33,10 @@
>
<div class="ugo-item" @click="toggle">
<div class="ugo-item-left">
<div class="ugo-item-title">{{ item.uid }} - {{ item.length }}条记录</div>
<div class="ugo-item-title">
<span>{{ item.uid }} - {{ item.length }}</span>
<span>{{ item.isUgc ? "颂愿记录" : "祈愿记录" }}</span>
</div>
<div class="ugo-item-sub">{{ item.time }}</div>
</div>
<div class="ugo-item-right">
@@ -55,6 +59,7 @@ import TOverlay from "@comp/app/t-overlay.vue";
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 { path } from "@tauri-apps/api";
import { open } from "@tauri-apps/plugin-dialog";
import TGLogger from "@utils/TGLogger.js";
@@ -68,7 +73,19 @@ type UgoUidProps = {
/** filePathImport导出路径 */
fpi?: string;
};
type UgoUidItem = { uid: string; length: number; time: string };
/**
* UID项
*/
type UgoUidItem = {
/** UID */
uid: string;
/** 数据条数 */
length: number;
/** 数据时间段 */
time: string;
/** 是否是颂愿数据 */
isUgc: boolean;
};
/** 兼容不同版本的导入 */
type UgoUidImportRaw =
| { isV4: true; data: TGApp.Plugins.UIGF.Schema4 }
@@ -98,7 +115,7 @@ watch(
async function getDefaultSavePath(): Promise<string> {
const tsNow = Math.floor(Date.now() / 1000);
return `${await path.downloadDir()}${path.sep()}UIGFv4.1_${tsNow}.json`;
return `${await path.downloadDir()}${path.sep()}UIGFv4.2_${tsNow}.json`;
}
async function refreshData(): Promise<void> {
@@ -147,7 +164,9 @@ async function refreshImport(): Promise<void> {
if (isV4) {
const read = await readUigf4Data(fp.value);
importRaw.value = { isV4: true, data: read };
data.value = read.hk4e.map(parseData4);
const hk4eUids = read.hk4e?.map(parseData4) ?? [];
const ugcUids = read.hk4e_ugc?.map(parseUgc) ?? [];
data.value = [...hk4eUids, ...ugcUids];
} else {
const read = await readUigfData(fp.value);
console.log(read.list.length);
@@ -172,6 +191,7 @@ function parseData(data: TGApp.Plugins.UIGF.Schema): UgoUidItem {
uid: data.info.uid,
length: data.list.length,
time: `${timestampToDate(Math.min(...timeList))} ~ ${timestampToDate(Math.max(...timeList))}`,
isUgc: false,
};
}
@@ -181,25 +201,52 @@ function parseData4(data: TGApp.Plugins.UIGF.GachaHk4e): UgoUidItem {
uid: data.uid.toString(),
length: data.list.length,
time: `${timestampToDate(Math.min(...timeList))} ~ ${timestampToDate(Math.max(...timeList))}`,
isUgc: false,
};
}
function parseUgc(data: TGApp.Plugins.UIGF.GachaUgc): UgoUidItem {
const timeList = data.list.map((item) => new Date(item.time).getTime());
return {
uid: data.uid.toString(),
length: data.list.length,
time: `${timestampToDate(Math.min(...timeList))} ~ ${timestampToDate(Math.max(...timeList))}`,
isUgc: true,
};
}
async function refreshExport(): Promise<void> {
const uidList = await TSUserGacha.getUidList();
const uidHk4e = await TSUserGacha.getUidList();
const uidUgc = await TSUserGachaB.getUidList();
const tmpData: Array<UgoUidItem> = [];
for (const uid of uidList) {
for (const uid of uidHk4e) {
const dataRaw = await TSUserGacha.record.all(uid);
tmpData.push(parseDataRaw(dataRaw));
tmpData.push(parseLocalHk4e(dataRaw));
}
for (const uid of uidUgc) {
const dataRaw = await TSUserGachaB.getGachaRecords(uid);
tmpData.push(parseLocalUgc(dataRaw));
}
data.value = tmpData;
}
function parseDataRaw(data: Array<TGApp.Sqlite.Gacha.Gacha>): UgoUidItem {
function parseLocalHk4e(data: Array<TGApp.Sqlite.Gacha.Gacha>): UgoUidItem {
const timeList = data.map((item) => new Date(item.time).getTime());
return {
uid: data[0].uid,
length: data.length,
time: `${timestampToDate(Math.min(...timeList))} ~ ${timestampToDate(Math.max(...timeList))}`,
isUgc: false,
};
}
function parseLocalUgc(data: Array<TGApp.Sqlite.Gacha.GachaB>): UgoUidItem {
const timeList = data.map((item) => new Date(item.time).getTime());
return {
uid: data[0].uid,
length: data.length,
time: `${timestampToDate(Math.min(...timeList))} ~ ${timestampToDate(Math.max(...timeList))}`,
isUgc: true,
};
}
@@ -222,13 +269,23 @@ async function handleImport(): Promise<void> {
if (importRaw.value.isV4) {
for (const item of selectedData.value) {
await showLoading.update(`正在导入UID: ${item.uid}`);
const dataFind = importRaw.value.data.hk4e.find((i) => i.uid.toString() === item.uid);
if (!item.isUgc) {
const dataFind = importRaw.value.data.hk4e?.find((i) => i.uid.toString() === item.uid);
if (!dataFind) {
showSnackbar.error(`未找到UID: ${item.uid}`);
await new Promise<void>((resolve) => setTimeout(resolve, 1000));
continue;
}
await TSUserGacha.mergeUIGF4(dataFind, true);
} else {
const dataFind = importRaw.value.data.hk4e_ugc?.find((i) => i.uid.toString() === item.uid);
if (!dataFind) {
showSnackbar.error(`未找到UID: ${item.uid}`);
await new Promise<void>((resolve) => setTimeout(resolve, 1000));
continue;
}
await TSUserGachaB.mergeUIGF4(dataFind, true);
}
}
} else {
const iUid = selectedData.value[0].uid;
@@ -251,7 +308,8 @@ async function handleExport(): Promise<void> {
`${selectedData.value.length}条UID - ${totalCnt}条记录`,
);
await exportUigf4Data(
selectedData.value.map((s) => s.uid.toString()),
selectedData.value.filter((i) => !i.isUgc).map((s) => s.uid.toString()),
selectedData.value.filter((i) => i.isUgc).map((s) => s.uid.toString()),
fp.value,
);
await showLoading.end();
@@ -348,8 +406,17 @@ async function handleExport(): Promise<void> {
}
.ugo-item-title {
position: relative;
display: flex;
align-items: center;
justify-content: flex-start;
column-gap: 8px;
font-family: var(--font-title);
font-size: 16px;
:last-child {
color: var(--tgc-od-red);
}
}
.ugo-item-sub {

View File

@@ -1,4 +1,4 @@
variantelevated<!-- 千星奇域祈愿记录页面 TODO处理活动卡池次数共享 -->
<!-- 千星奇域祈愿记录页面 TODO处理活动卡池次数共享 -->
<template>
<v-app-bar>
<template #prepend>
@@ -34,6 +34,31 @@ variantelevated<!-- 千星奇域祈愿记录页面 TODO处理活动卡池次
>
全量刷新
</v-btn>
<v-btn
class="gb-top-btn"
prepend-icon="mdi-import"
variant="elevated"
@click="importUigf()"
>
导入
</v-btn>
<v-btn
class="gb-top-btn"
prepend-icon="mdi-export"
variant="elevated"
@click="exportUigf()"
>
导出
</v-btn>
<v-btn
class="gb-top-btn"
prepend-icon="mdi-database"
title="将数据库中非中文数据转换为中文"
variant="elevated"
@click="checkData()"
>
检测数据
</v-btn>
<v-btn
class="gb-top-btn"
prepend-icon="mdi-delete"
@@ -59,6 +84,7 @@ variantelevated<!-- 千星奇域祈愿记录页面 TODO处理活动卡池次
</v-window-item>
</v-window>
</div>
<UgoUid v-model="ovShow" :fpi="ovFpi" :mode="ovMode" />
</template>
<script lang="ts" setup>
import showDialog from "@comp/func/dialog.js";
@@ -66,11 +92,14 @@ import showLoading from "@comp/func/loading.js";
import showSnackbar from "@comp/func/snackbar.js";
import GbrOverview from "@comp/userGacha/gbr-overview.vue";
import GbrTable from "@comp/userGacha/gbr-table.vue";
import UgoUid from "@comp/userGacha/ugo-uid.vue";
import hk4eReq from "@req/hk4eReq.js";
import takumiReq from "@req/takumiReq.js";
import TSUserGachaB from "@Sqlm/userGachaB.js";
import useAppStore from "@store/app.js";
import useUserStore from "@store/user.js";
import { path } from "@tauri-apps/api";
import { open } from "@tauri-apps/plugin-dialog";
import TGLogger from "@utils/TGLogger.js";
import { storeToRefs } from "pinia";
import { onMounted, ref, shallowRef, watch } from "vue";
@@ -80,6 +109,10 @@ const router = useRouter();
const { isLogin } = storeToRefs(useAppStore());
const { account, cookie } = storeToRefs(useUserStore());
const ovMode = ref<"export" | "import">("import");
const ovShow = ref<boolean>(false);
const ovFpi = ref<string>();
const authkey = ref<string>("");
const uidCur = ref<string>();
const tab = ref<string>("overview");
@@ -219,27 +252,26 @@ async function refreshGachaPool(
await new Promise<void>((resolve) => setTimeout(resolve, 1000));
break;
}
if (gachaRes.length === 0) {
// if (force) {
// await showLoading.update(`正在清理${label}数据`);
// if (gachaDataMap) {
// await TSUserGacha.cleanGachaRecords(account.value.gameUid, type, gachaDataMap);
// }
// }
break;
}
if (gachaRes.length === 0) break;
const uigfList: Array<TGApp.Plugins.UIGF.GachaItemB> = [];
if (force) await showLoading.update(`[${gachaName}] 第${page}页,${gachaRes.length}`);
for (const item of gachaRes) {
if (!force) {
await showLoading.update(`[${item.item_type}][${item.time}] ${item.item_name}`);
}
if (force) {
// if (!gachaDataMap) gachaDataMap = {};
// if (!gachaDataMap[item.time]) gachaDataMap[item.time] = [];
// gachaDataMap[item.time].push(item.id.toString());
const tempItem: TGApp.Plugins.UIGF.GachaItemB = {
id: item.id,
item_id: item.item_id,
item_name: item.item_name,
item_type: item.item_type,
op_gacha_type: item.op_gacha_type,
rank_type: item.rank_type,
schedule_id: item.schedule_id,
time: item.time,
};
uigfList.push(tempItem);
}
}
await TSUserGachaB.insertGachaList(gachaRes);
await TSUserGachaB.insertGachaList(account.value.gameUid, uigfList);
if (!force && gachaRes.some((i) => i.id.toString() === endId.toString())) break;
reqId = gachaRes[gachaRes.length - 1].id.toString();
if (force) await new Promise<void>((resolve) => setTimeout(resolve, 1000));
@@ -283,6 +315,39 @@ async function deleteGacha(): Promise<void> {
await new Promise<void>((resolve) => setTimeout(resolve, 1500));
window.location.reload();
}
async function importUigf(): Promise<void> {
await TGLogger.Info(`[UserGachaB][importUigf] 导入祈愿数据`);
const selectedFile = await open({
multiple: false,
title: "导入UIGF文件",
filters: [{ name: "UIGF JSON", extensions: ["json"] }],
defaultPath: await path.downloadDir(),
directory: false,
});
if (selectedFile === null) {
showSnackbar.cancel("已取消文件选择");
return;
}
ovFpi.value = selectedFile;
ovMode.value = "import";
ovShow.value = true;
}
async function exportUigf(): Promise<void> {
if (!uidCur.value) {
showSnackbar.error("未获取到 UID");
return;
}
await TGLogger.Info(`[UserGachaB][${uidCur.value}][exportUigf] 导出祈愿数据`);
ovMode.value = "export";
ovShow.value = true;
}
async function checkData(): Promise<void> {
// TODO
showSnackbar.warn("暂未支持");
}
</script>
<style lang="scss" scoped>
.gb-top-title {

View File

@@ -1,63 +1,59 @@
/**
* 用户祈愿模块
* @since Beta v0.9.1
* @since Beta v0.9.5
*/
import showLoading from "@comp/func/loading.js";
import showSnackbar from "@comp/func/snackbar.js";
import { path } from "@tauri-apps/api";
import { exists, mkdir, readDir } from "@tauri-apps/plugin-fs";
import TGLogger from "@utils/TGLogger.js";
import { getWikiBrief, timestampToDate } from "@utils/toolFunc.js";
import { exportUigf4Data, readUigf4Data, readUigfData, verifyUigfData } from "@utils/UIGF.js";
import { getUtc8Time, getWikiBrief, timestampToDate } from "@utils/toolFunc.js";
import TGSqlite from "../index.js";
/**
* 获取导入 Sql
* @since Beta v.6.0
* 导入物品
* @since Beta v0.9.5
* @param uid - UID
* @param gacha - UIGF数据
* @returns sql
* @param item - UIGF数据
* @returns Promise<void>
*/
function getInsertSql(uid: string, gacha: TGApp.Plugins.UIGF.GachaItem): string {
return `
INSERT INTO GachaRecords (uid, gachaType, itemId, count, time, name, type, rank, id, uigfType, updated)
VALUES ('${uid}', '${gacha.gacha_type}', '${gacha.item_id ?? null}', '${gacha.count ?? null}', '${gacha.time}',
'${gacha.name}', '${gacha.item_type ?? null}', '${gacha.rank_type ?? null}', '${gacha.id}',
'${gacha.uigf_gacha_type}', datetime('now', 'localtime')) ON CONFLICT (id)
DO
UPDATE
SET uid = '${uid}',
gachaType = '${gacha.gacha_type}',
uigfType = '${gacha.uigf_gacha_type}',
time = '${gacha.time}',
itemId = '${gacha.item_id ?? null}',
count = '${gacha.count ?? null}',
name = '${gacha.name}',
type = '${gacha.item_type ?? null}',
rank = '${gacha.rank_type ?? null}',
updated = datetime('now', 'localtime');
`;
}
/**
* 传入时间字符串跟对应时区转成utc8时间字符串
* @since Beta v0.7.5
* @param time - 时间字符串
* @param timezone - 时区
* @returns 转换后的时间戳
*/
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());
async function insertGachaItem(uid: string, item: TGApp.Plugins.UIGF.GachaItem): Promise<void> {
const db = await TGSqlite.getDB();
const updateTime = timestampToDate(Date.now());
await db.execute(
`INSERT INTO GachaRecords(uid, gachaType, itemId, count, time,
name, type, rank, id, uigfType, updated)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
ON CONFLICT DO UPDATE
SET uid = $1,
gachaType = $2,
itemId = $3,
count = $4,
time = $5,
name = $6,
type = $7,
rank = $8,
uigfType = $10,
updated = $11;
`,
[
uid,
item.gacha_type,
item.item_id ?? null,
item.count ?? 1,
item.time,
item.name,
item.item_type ?? null,
item.rank_type ?? null,
item.id,
item.uigf_gacha_type,
updateTime,
],
);
}
/**
* 转换祈愿数据,防止多语言
* @since Beta v0.7.5
* @since Beta v0.9.5
* @param gacha - UIGF数据
* @param timezone - 时区
* @returns 转换后的数据
@@ -67,7 +63,7 @@ function transGacha(
timezone: number = 8,
): TGApp.Plugins.UIGF.GachaItem {
const find = getWikiBrief(gacha.item_id);
if (!find) return gacha;
if (!find) return { ...gacha, time: getUtc8Time(gacha.time, timezone) };
return {
gacha_type: gacha.gacha_type,
item_id: gacha.item_id,
@@ -246,7 +242,7 @@ async function cleanGachaRecords(
/**
* 合并祈愿数据
* @since Beta v0.9.0
* @since Beta v0.9.5
* @param uid - UID
* @param data - UIGF数据
* @param showProgress - 是否显示进度
@@ -257,7 +253,6 @@ async function mergeUIGF(
data: Array<TGApp.Plugins.UIGF.GachaItem>,
showProgress: boolean = false,
): Promise<void> {
const db = await TGSqlite.getDB();
let cnt = 0;
const len = data.length;
let progress = 0;
@@ -272,9 +267,7 @@ async function mergeUIGF(
}, 1000);
}
for (const gacha of data) {
const trans = transGacha(gacha);
const sql = getInsertSql(uid, trans);
await db.execute(sql);
await insertGachaItem(uid, transGacha(gacha));
cnt++;
}
if (timer) {
@@ -285,8 +278,8 @@ async function mergeUIGF(
}
/**
* 合并祈愿数据v4.0
* @since Beta v0.9.0
* 合并祈愿数据v4.x
* @since Beta v0.9.5
* @param data - UIGF数据
* @param showProgress - 是否显示进度
* @returns 无返回值
@@ -295,7 +288,6 @@ async function mergeUIGF4(
data: TGApp.Plugins.UIGF.GachaHk4e,
showProgress: boolean = false,
): Promise<void> {
const db = await TGSqlite.getDB();
let cnt: number = 0;
const len = data.list.length;
let progress: number = 0;
@@ -304,15 +296,13 @@ async function mergeUIGF4(
timer = setInterval(async () => {
progress = Math.round((cnt / len) * 100 * 100) / 100;
const current = data.list[cnt]?.time ?? "";
const name = data.list[cnt]?.name ?? "";
const name = data.list[cnt]?.name ?? data.list[cnt]?.item_id;
const rank = data.list[cnt]?.rank_type ?? "0";
await showLoading.update(`[${progress}%][${current}] ${"⭐".repeat(Number(rank))}-${name}`);
}, 1000);
}
for (const gacha of data.list) {
const trans = transGacha(gacha, data.timezone);
const sql = getInsertSql(data.uid.toString(), trans);
await db.execute(sql);
await insertGachaItem(data.uid.toString(), transGacha(gacha, data.timezone));
cnt++;
}
if (timer) {
@@ -322,85 +312,6 @@ async function mergeUIGF4(
}
}
/**
* 备份祈愿数据
* @since Beta v0.9.0
* @param dir - 备份目录
* @returns 无返回值
*/
async function backUpUigf(dir: string): Promise<void> {
if (!(await exists(dir))) {
await TGLogger.Warn("不存在指定的祈愿备份目录,即将创建");
await mkdir(dir, { recursive: true });
}
const uidList = await getUidList();
const savePath = `${dir}${path.sep()}UIGF4.json`;
await exportUigf4Data(uidList, savePath);
await TGLogger.Info("祈愿数据备份完成");
}
/**
* 恢复祈愿数据
* @since Beta v0.9.0
* @param dir - 备份目录
* @returns 是否恢复成功
*/
async function restoreUigf(dir: string): Promise<boolean> {
if (!(await exists(dir))) {
await TGLogger.Warn("不存在指定的祈愿备份目录");
return false;
}
let cnt = 0;
// 旧版备份文件 UIGF_{{uid}}
const legacyReg = /^UIGF_\d+\.json$/;
const legacyFiles = (await readDir(dir)).filter(
(item) => item.isFile && legacyReg.test(item.name),
);
if (legacyFiles.length > 0) {
try {
for (const file of legacyFiles) {
await showLoading.update(`祈愿文件:${file.name}`);
const filePath = `${dir}${path.sep()}${file.name}`;
const check = await verifyUigfData(filePath);
if (!check) {
await showLoading.update(`UIGF数据校验失败`);
await TGLogger.Warn(`UIGF数据校验失败${filePath}`);
continue;
}
const data = await readUigfData(filePath);
const uid = data.info.uid;
await showLoading.update(`正在导入${data.info.uid}${data.list.length}条祈愿数据`);
await mergeUIGF(uid, data.list, true);
cnt++;
}
} catch (e) {
await TGLogger.Error(`恢复祈愿数据失败${dir}`);
await TGLogger.Error(typeof e === "string" ? e : JSON.stringify(e));
}
}
const filePath = `${dir}${path.sep()}UIGF4.json`;
if (!(await exists(filePath))) {
await TGLogger.Warn(`未检测到UIGF4备份文件`);
} else {
try {
const check = await verifyUigfData(filePath, true);
if (!check) {
await TGLogger.Warn(`UIGF数据校验失败${filePath}`);
} else {
const data = await readUigf4Data(filePath);
for (const uidData of data.hk4e) {
await showLoading.update(`正在导入${uidData.uid}${uidData.list.length}条祈愿记录`);
await mergeUIGF4(uidData, true);
}
}
} catch (e) {
await TGLogger.Error(`恢复祈愿数据失败${dir}`);
await TGLogger.Error(typeof e === "string" ? e : JSON.stringify(e));
}
}
return cnt > 0;
}
/**
* 更新单条数据ID
* @since Beta v0.9.0
@@ -420,8 +331,6 @@ const TSUserGacha = {
cleanGachaRecords,
mergeUIGF,
mergeUIGF4,
backUpUigf,
restoreUigf,
update: {
itemId: updateItemIdById,
},

View File

@@ -1,56 +1,132 @@
/**
* 千星奇域祈愿模块
* @since Beta v0.8.4
* @since Beta v0.9.5
*/
import showSnackbar from "@comp/func/snackbar.js";
import showLoading from "@comp/func/loading.js";
import TGSqlite from "@Sql/index.js";
import { exists, mkdir } from "@tauri-apps/plugin-fs";
import TGLogger from "@utils/TGLogger.js";
import { getUtc8Time, timestampToDate } from "@utils/toolFunc.js";
import { AppGachaBData } from "@/data/index.js";
/**
* 获取导入 Sql
* @since Beta v0.8.4
* @param gacha - 抽卡记录数据
* @returns sql
* 插入颂愿数据
* @since Beta v0.9.5
* @param uid - UID
* @param item - 颂愿数据
* @returns 无返回值
*/
function getInsertSql(gacha: TGApp.Game.Gacha.GachaBItem): string {
return `
INSERT INTO GachaBRecords(id, uid, region, scheduleId, gachaType,
opGachaType, time, itemId, name, type,
rank, isUp, updated)
VALUES ('${gacha.id}', '${gacha.uid}', '${gacha.region}', '${gacha.schedule_id}',
'${gacha.op_gacha_type === "1000" ? "1000" : "2000"}', '${gacha.op_gacha_type}', '${gacha.time}',
'${gacha.item_id}', '${gacha.item_name}', '${gacha.item_type}',
'${gacha.rank_type}', '${gacha.is_up}', datetime('now', 'localtime'))
async function insertGachaItem(uid: string, item: TGApp.Plugins.UIGF.GachaItemB): Promise<void> {
const db = await TGSqlite.getDB();
const gachaType = item.op_gacha_type === "1000" ? "1000" : "2000";
const updateTime = timestampToDate(Date.now());
await db.execute(
`
INSERT INTO GachaBRecords(id, uid, scheduleId, gachaType, opGachaType, time,
itemId, name, type, rank, updated)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
ON CONFLICT (id)
DO UPDATE
SET uid = '${gacha.uid}',
region = '${gacha.region}',
scheduleId = '${gacha.schedule_id}',
gachaType = '${gacha.op_gacha_type === "1000" ? "1000" : "2000"}',
opGachaType = '${gacha.op_gacha_type}',
time = '${gacha.time}',
itemId = '${gacha.item_id}',
name = '${gacha.item_name}',
type = '${gacha.item_type}',
rank = '${gacha.rank_type}',
isUp = '${gacha.is_up}',
updated = datetime('now', 'localtime');
`;
SET uid = $2,
scheduleId = $3,
gachaType = $4,
opGachaType = $5,
time = $6,
itemId = $7,
name = $8,
type = $9,
rank = $10,
updated = $11;
`,
[
item.id,
uid,
item.schedule_id,
gachaType,
item.op_gacha_type,
item.time,
item.item_id,
item.item_name,
item.item_type,
item.rank_type,
updateTime,
],
);
}
/**
* 插入列表数据
* @since Beta v0.8.4
* @since Beta v0.9.5
* @param uid - UID
* @param list - 抽卡记录列表
* @returns 无返回值
*/
async function insertGachaList(list: Array<TGApp.Game.Gacha.GachaBItem>): Promise<void> {
const db = await TGSqlite.getDB();
async function insertGachaList(
uid: string,
list: Array<TGApp.Plugins.UIGF.GachaItemB>,
): Promise<void> {
for (const gacha of list) {
const sql = getInsertSql(gacha);
await db.execute(sql);
await insertGachaItem(uid, gacha);
}
}
/**
* 转换颂愿数据,防止多语言
* @since Beta v0.9.5
* @param gacha - 颂愿数据
* @param timezone - 时区
* @returns 转换后的数据
*/
function transGacha(
gacha: TGApp.Plugins.UIGF.GachaItemB,
timezone: number = 8,
): TGApp.Plugins.UIGF.GachaItemB {
const find = AppGachaBData.find((i) => i.id === gacha.item_id);
if (!find) return gacha;
return {
id: gacha.id,
item_id: gacha.item_id,
item_name: find.name,
item_type: find.type,
op_gacha_type: gacha.op_gacha_type,
rank_type: find.rank.toString(),
schedule_id: gacha.schedule_id,
time: getUtc8Time(gacha.time, timezone),
};
}
/**
* 插入UIGF4数据
* @since Beta v0.9.5
* @param data - UIGF数据
* @param showProgress - 是否显示进度
* @returns 无返回值
*/
async function mergeUIGF4(
data: TGApp.Plugins.UIGF.GachaUgc,
showProgress: boolean = false,
): Promise<void> {
let cnt: number = 0;
const len = data.list.length;
let progress: number = 0;
let timer: NodeJS.Timeout | null = null;
if (showProgress) {
timer = setInterval(async () => {
progress = Math.round((cnt / len) * 100 * 100) / 100;
const current = data.list[cnt].time ?? "";
const name = data.list[cnt].item_name ?? "";
const rank = data.list[cnt].rank_type ?? "0";
await showLoading.update(`[${progress}%][${current}] ${"⭐".repeat(Number(rank))}-${name}`);
}, 1000);
}
for (const gacha of data.list) {
await insertGachaItem(data.uid.toString(), transGacha(gacha, data.timezone));
cnt++;
}
if (timer) {
clearInterval(timer);
progress = 100;
await showLoading.update(`[${progress}%] 完成`);
}
}
@@ -105,35 +181,6 @@ async function getGachaRecords(
return await db.select("SELECT * FROM GachaBRecords WHERE uid = ?;", [uid]);
}
/**
* 备份祈愿数据
* @since Beta v0.8.4
* @param dir - 备份目录
* @remarks 等UIGF标准最终确定后与TSUserGacha合并
*/
async function backUpUigf(dir: string): Promise<void> {
if (!(await exists(dir))) {
await TGLogger.Warn("不存在指定的祈愿备份目录,即将创建");
await mkdir(dir, { recursive: true });
}
showSnackbar.error(`千星奇域祈愿数据备份功能尚未实现,请耐心等待后续版本更新。`);
}
/**
* 恢复祈愿数据
* @since Beta v0.8.4
* @param dir - 恢复目录
* @remarks 等UIGF标准最终确定后与TSUserGacha合并
* @returns 是否恢复成功
*/
async function restoreUigf(dir: string): Promise<boolean> {
if (!(await exists(dir))) {
await TGLogger.Warn("不存在指定的祈愿备份目录");
return false;
}
return true;
}
/**
* 删除用户祈愿数据
* @since Beta v0.8.4
@@ -145,18 +192,13 @@ async function deleteRecords(uid: string): Promise<void> {
await db.execute("DELETE FROM GachaBRecords WHERE uid = ?;", [uid]);
}
/**
* 千星奇域祈愿模块
* @since Beta v0.8.4
*/
const TSUserGachaB = {
getUidList,
getGachaCheck,
getGachaRecords,
insertGachaList,
backUpUigf,
restoreUigf,
deleteRecords,
mergeUIGF4,
};
export default TSUserGachaB;

View File

@@ -182,12 +182,12 @@ create table if not exists GachaRecords
updated text
);
-- @brief 创建千星奇域祈愿数据表
-- @brief 创建愿数据表
create table if not exists GachaBRecords
(
id text primary key not null,
uid text,
region text,
region text, -- @deprecated
scheduleId text,
gachaType text,
opGachaType text,
@@ -196,7 +196,7 @@ create table if not exists GachaBRecords
name text,
type text,
rank text,
isUp text,
isUp text, -- @deprecated
updated text
);

View File

@@ -1,6 +1,6 @@
/**
* 数据库抽卡记录相关类型定义文件
* @since Beta v0.9.1
* @since Beta v0.9.5
*/
declare namespace TGApp.Sqlite.Gacha {
@@ -57,7 +57,10 @@ declare namespace TGApp.Sqlite.Gacha {
id: string;
/** UID */
uid: string;
/** 服务器区域 */
/**
* 服务器区域
* @deprecated 弃用
*/
region: string;
/** 排期 ID */
scheduleId: string;
@@ -99,7 +102,10 @@ declare namespace TGApp.Sqlite.Gacha {
type: string;
/** 抽卡物品星级 */
rank: string;
/** 是否是 UP 物品 */
/**
* 是否是 UP 物品
* @deprecated 弃用
*/
isUp: 0 | 1;
/** 数据库更新时间 */
updated: string;

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,24 +75,23 @@ 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 {
return data.map((gacha) => ({
gacha_type: gacha.gachaType,
item_id: gacha.itemId,
count: gacha.count,
@@ -101,13 +101,36 @@ function convertDataToUigf(
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());
}

View File

@@ -9,7 +9,8 @@
"@see": true,
"@since": true,
"@TODO": true,
"@typeParam": true
"@typeParam": true,
"@deprecated": true
},
"tagDefinitions": [
{