mirror of
https://github.com/BTMuli/TeyvatGuide.git
synced 2026-03-28 05:59:46 +08:00
437 lines
13 KiB
TypeScript
437 lines
13 KiB
TypeScript
/**
|
||
* 用户祈愿模块
|
||
* @since Beta v0.9.1
|
||
*/
|
||
|
||
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 TGSqlite from "../index.js";
|
||
|
||
/**
|
||
* 获取导入 Sql
|
||
* @since Beta v.6.0
|
||
* @param uid - UID
|
||
* @param gacha - UIGF数据
|
||
* @returns sql
|
||
*/
|
||
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());
|
||
}
|
||
|
||
/**
|
||
* 转换祈愿数据,防止多语言
|
||
* @since Beta v0.7.5
|
||
* @param gacha - UIGF数据
|
||
* @param timezone - 时区
|
||
* @returns 转换后的数据
|
||
*/
|
||
function transGacha(
|
||
gacha: TGApp.Plugins.UIGF.GachaItem,
|
||
timezone: number = 8,
|
||
): TGApp.Plugins.UIGF.GachaItem {
|
||
const find = getWikiBrief(gacha.item_id);
|
||
if (!find) return gacha;
|
||
return {
|
||
gacha_type: gacha.gacha_type,
|
||
item_id: gacha.item_id,
|
||
count: gacha.count ?? "1",
|
||
time: getUtc8Time(gacha.time, timezone),
|
||
name: find.name,
|
||
item_type: "element" in find ? "角色" : "武器",
|
||
rank_type: find.star.toString(),
|
||
id: gacha.id,
|
||
uigf_gacha_type: gacha.uigf_gacha_type,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 获取数据库的uid列表
|
||
* @since Beta v0.4.7
|
||
* @returns uid列表
|
||
*/
|
||
async function getUidList(): Promise<Array<string>> {
|
||
const db = await TGSqlite.getDB();
|
||
type resType = Array<{ uid: string }>;
|
||
const res = await db.select<resType>("SELECT DISTINCT uid FROM GachaRecords;");
|
||
return res.map((i) => i.uid);
|
||
}
|
||
|
||
/**
|
||
* 获取检测增量更新的记录 ID
|
||
* @since Beta v0.4.7
|
||
* @param uid - UID
|
||
* @param type - 类型
|
||
* @returns ID
|
||
*/
|
||
async function getGachaCheck(uid: string, type: string): Promise<string | undefined> {
|
||
const db = await TGSqlite.getDB();
|
||
type resType = Array<{ id: string }>;
|
||
const res = await db.select<resType>(
|
||
"SELECT id FROM GachaRecords WHERE uid = ? AND gachaType = ? ORDER BY id DESC LIMIT 1;",
|
||
[uid, type],
|
||
);
|
||
if (res.length === 0) return undefined;
|
||
return res[0].id;
|
||
}
|
||
|
||
/**
|
||
* 获取用户祈愿记录
|
||
* @since Beta v0.4.7
|
||
* @param uid - UID
|
||
* @returns 祈愿记录
|
||
*/
|
||
async function getGachaRecords(uid: string): Promise<Array<TGApp.Sqlite.Gacha.Gacha>> {
|
||
const db = await TGSqlite.getDB();
|
||
return await db.select("SELECT * FROM GachaRecords WHERE uid = ?;", [uid]);
|
||
}
|
||
|
||
/**
|
||
* 获取指定EndId后的祈愿记录
|
||
* @since Beta v0.9.1
|
||
* @param uid - UID
|
||
* @param pool - Pool
|
||
* @param endId - endId
|
||
* @returns 祈愿记录
|
||
*/
|
||
async function getGachaRecordsByEndId(
|
||
uid: string,
|
||
pool: string,
|
||
endId?: string,
|
||
): Promise<Array<TGApp.Sqlite.Gacha.Gacha>> {
|
||
const db = await TGSqlite.getDB();
|
||
if (endId && endId !== "0") {
|
||
return await db.select(
|
||
"SELECT * FROM GachaRecords WHERE uid = ? AND uigfType = ? AND id > ?;",
|
||
[uid, pool, endId],
|
||
);
|
||
}
|
||
return await db.select("SELECT * FROM GachaRecords WHERE uid = ? AND uigfType = ?;", [uid, pool]);
|
||
}
|
||
|
||
/**
|
||
* 获取用户祈愿记录,并按照日期进行分组排序
|
||
* @since Beta v0.6.8
|
||
* @param uid - UID
|
||
* @param type - 类型
|
||
* @returns 日期分组的祈愿记录
|
||
*/
|
||
async function getGachaRecordsByDate(
|
||
uid: string,
|
||
type?: string,
|
||
): Promise<Record<string, Array<TGApp.Sqlite.Gacha.Gacha>>> {
|
||
const db = await TGSqlite.getDB();
|
||
type resType = Array<TGApp.Sqlite.Gacha.Gacha>;
|
||
let res: resType;
|
||
if (type) {
|
||
res = await db.select<resType>(
|
||
"SELECT * FROM GachaRecords WHERE uid = ? AND gachaType = ? ORDER BY time;",
|
||
[uid, type],
|
||
);
|
||
} else {
|
||
res = await db.select<resType>("SELECT * FROM GachaRecords WHERE uid = ? ORDER BY time;", [
|
||
uid,
|
||
]);
|
||
}
|
||
const map: Record<string, Array<TGApp.Sqlite.Gacha.Gacha>> = {};
|
||
for (const item of res) {
|
||
// key 是 yyyy-MM-dd hh:mm:ss,按照日期分组
|
||
const key = item.time.split(" ")[0];
|
||
if (!map[key]) map[key] = [];
|
||
map[key].push(item);
|
||
}
|
||
return map;
|
||
}
|
||
|
||
/**
|
||
* 获取指定卡池的祈愿记录
|
||
* @since Beta v0.9.1
|
||
* @param pool - 卡池信息
|
||
* @param uid - 用户UID
|
||
*/
|
||
async function getGachaRecordsByPool(
|
||
pool: TGApp.App.Gacha.PoolItem,
|
||
uid: string,
|
||
): Promise<Array<TGApp.Sqlite.Gacha.Gacha>> {
|
||
console.log(pool, uid);
|
||
const db = await TGSqlite.getDB();
|
||
type resType = Array<TGApp.Sqlite.Gacha.Gacha>;
|
||
return await db.select<resType>(
|
||
"SELECT * FROM GachaRecords WHERE uid = ? AND gachaType = ? AND time >= ? AND time <= ?;",
|
||
[
|
||
uid.toString(),
|
||
pool.type.toString(),
|
||
pool.from.slice(0, 19).replace("T", " "),
|
||
pool.to.slice(0, 19).replace("T", " "),
|
||
],
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 删除指定UID的祈愿记录
|
||
* @since Beta v0.4.7
|
||
* @param uid - UID
|
||
* @returns 无返回值
|
||
*/
|
||
async function deleteGachaRecords(uid: string): Promise<void> {
|
||
const db = await TGSqlite.getDB();
|
||
await db.execute("DELETE FROM GachaRecords WHERE uid = ?;", [uid]);
|
||
}
|
||
|
||
/**
|
||
* 清理祈愿记录
|
||
* @since Beta v0.6.4
|
||
* @param uid - UID
|
||
* @param pool - 池子
|
||
* @param map - 祈愿数据
|
||
* @returns 无返回值
|
||
*/
|
||
async function cleanGachaRecords(
|
||
uid: string,
|
||
pool: string,
|
||
map: Record<string, Array<string>>,
|
||
): Promise<void> {
|
||
const db = await TGSqlite.getDB();
|
||
for (const [time, ids] of Object.entries(map)) {
|
||
const sql = `DELETE
|
||
FROM GachaRecords
|
||
WHERE uid = '${uid}'
|
||
AND gachaType = '${pool}'
|
||
AND time = '${time}'
|
||
AND id NOT IN (${ids.map((i) => `'${i}'`).join(",")});
|
||
`;
|
||
const res = await db.execute(sql);
|
||
if (res.rowsAffected > 0) {
|
||
showSnackbar.success(`[${uid}][${pool}][${time}]清理了${res.rowsAffected}条祈愿记录`);
|
||
await new Promise<void>((resolve) => setTimeout(resolve, 1500));
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 合并祈愿数据
|
||
* @since Beta v0.9.0
|
||
* @param uid - UID
|
||
* @param data - UIGF数据
|
||
* @param showProgress - 是否显示进度
|
||
* @returns 无返回值
|
||
*/
|
||
async function mergeUIGF(
|
||
uid: string,
|
||
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;
|
||
let timer: NodeJS.Timeout | null = null;
|
||
if (showProgress) {
|
||
timer = setInterval(async () => {
|
||
progress = Math.round((cnt / len) * 100 * 100) / 100;
|
||
const current = data[cnt]?.time ?? "";
|
||
const name = data[cnt]?.name ?? "";
|
||
const rank = data[cnt]?.rank_type ?? "0";
|
||
await showLoading.update(`[${progress}%][${current}] ${"⭐".repeat(Number(rank))}-${name}`);
|
||
}, 1000);
|
||
}
|
||
for (const gacha of data) {
|
||
const trans = transGacha(gacha);
|
||
const sql = getInsertSql(uid, trans);
|
||
await db.execute(sql);
|
||
cnt++;
|
||
}
|
||
if (timer) {
|
||
clearInterval(timer);
|
||
progress = 100;
|
||
await showLoading.update(`[${progress}%] 完成`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 合并祈愿数据(v4.0)
|
||
* @since Beta v0.9.0
|
||
* @param data - UIGF数据
|
||
* @param showProgress - 是否显示进度
|
||
* @returns 无返回值
|
||
*/
|
||
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;
|
||
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]?.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) {
|
||
const trans = transGacha(gacha, data.timezone);
|
||
const sql = getInsertSql(data.uid.toString(), trans);
|
||
await db.execute(sql);
|
||
cnt++;
|
||
}
|
||
if (timer) {
|
||
clearInterval(timer);
|
||
progress = 100;
|
||
await showLoading.update(`[${progress}%] 完成`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 备份祈愿数据
|
||
* @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
|
||
* @param raw - 原始数据
|
||
* @param itemId - 物品ID
|
||
* @returns 无返回值
|
||
*/
|
||
async function updateItemIdById(raw: TGApp.Sqlite.Gacha.Gacha, itemId: string): Promise<void> {
|
||
const db = await TGSqlite.getDB();
|
||
await db.execute("UPDATE GachaRecords SET itemId = ? WHERE id = ?;", [itemId.toString(), raw.id]);
|
||
}
|
||
|
||
const TSUserGacha = {
|
||
getUidList,
|
||
getGachaCheck,
|
||
deleteGachaRecords,
|
||
cleanGachaRecords,
|
||
mergeUIGF,
|
||
mergeUIGF4,
|
||
backUpUigf,
|
||
restoreUigf,
|
||
update: {
|
||
itemId: updateItemIdById,
|
||
},
|
||
record: {
|
||
all: getGachaRecords,
|
||
endId: getGachaRecordsByEndId,
|
||
time: getGachaRecordsByDate,
|
||
pool: getGachaRecordsByPool,
|
||
},
|
||
};
|
||
|
||
export default TSUserGacha;
|