feat: QQBot映射改为字典维护
This commit is contained in:
parent
c73084b4d1
commit
cf2c20ba22
@ -543,6 +543,101 @@ ON DUPLICATE KEY UPDATE
|
||||
`status` = VALUES(`status`),
|
||||
`is_deleted` = 0;
|
||||
|
||||
INSERT INTO `admin_dict` (`id`, `dict_code`, `label`, `value`, `children_code`, `sort`, `status`)
|
||||
VALUES
|
||||
(2041700000000300601, 'FFLOGS_ENCOUNTER_LABEL', 'M9S Vamp Fatale', 'vampfatale', NULL, 1, 1),
|
||||
(2041700000000300602, 'FFLOGS_ENCOUNTER_LABEL', 'M10S Red Hot and Deep Blue', 'redhotanddeepblue', NULL, 2, 1),
|
||||
(2041700000000300603, 'FFLOGS_ENCOUNTER_LABEL', 'M11S The Tyrant', 'thetyrant', NULL, 3, 1),
|
||||
(2041700000000300604, 'FFLOGS_ENCOUNTER_LABEL', 'M12S P1 Lindwurm', 'lindwurm', NULL, 4, 1),
|
||||
(2041700000000300605, 'FFLOGS_ENCOUNTER_LABEL', 'M12S P2 Lindwurm II', 'lindwurmii', NULL, 5, 1),
|
||||
(2041700000000300701, 'FFLOGS_JOB_LABEL', '骑士', 'paladin', NULL, 1, 1),
|
||||
(2041700000000300702, 'FFLOGS_JOB_LABEL', '战士', 'warrior', NULL, 2, 1),
|
||||
(2041700000000300703, 'FFLOGS_JOB_LABEL', '暗黑骑士', 'darkknight', NULL, 3, 1),
|
||||
(2041700000000300704, 'FFLOGS_JOB_LABEL', '绝枪战士', 'gunbreaker', NULL, 4, 1),
|
||||
(2041700000000300705, 'FFLOGS_JOB_LABEL', '白魔法师', 'whitemage', NULL, 5, 1),
|
||||
(2041700000000300706, 'FFLOGS_JOB_LABEL', '学者', 'scholar', NULL, 6, 1),
|
||||
(2041700000000300707, 'FFLOGS_JOB_LABEL', '占星术士', 'astrologian', NULL, 7, 1),
|
||||
(2041700000000300708, 'FFLOGS_JOB_LABEL', '贤者', 'sage', NULL, 8, 1),
|
||||
(2041700000000300709, 'FFLOGS_JOB_LABEL', '武僧', 'monk', NULL, 9, 1),
|
||||
(2041700000000300710, 'FFLOGS_JOB_LABEL', '龙骑士', 'dragoon', NULL, 10, 1),
|
||||
(2041700000000300711, 'FFLOGS_JOB_LABEL', '忍者', 'ninja', NULL, 11, 1),
|
||||
(2041700000000300712, 'FFLOGS_JOB_LABEL', '武士', 'samurai', NULL, 12, 1),
|
||||
(2041700000000300713, 'FFLOGS_JOB_LABEL', '钐镰客', 'reaper', NULL, 13, 1),
|
||||
(2041700000000300714, 'FFLOGS_JOB_LABEL', '蝰蛇剑士', 'viper', NULL, 14, 1),
|
||||
(2041700000000300715, 'FFLOGS_JOB_LABEL', '吟游诗人', 'bard', NULL, 15, 1),
|
||||
(2041700000000300716, 'FFLOGS_JOB_LABEL', '机工士', 'machinist', NULL, 16, 1),
|
||||
(2041700000000300717, 'FFLOGS_JOB_LABEL', '舞者', 'dancer', NULL, 17, 1),
|
||||
(2041700000000300718, 'FFLOGS_JOB_LABEL', '黑魔法师', 'blackmage', NULL, 18, 1),
|
||||
(2041700000000300719, 'FFLOGS_JOB_LABEL', '召唤师', 'summoner', NULL, 19, 1),
|
||||
(2041700000000300720, 'FFLOGS_JOB_LABEL', '赤魔法师', 'redmage', NULL, 20, 1),
|
||||
(2041700000000300721, 'FFLOGS_JOB_LABEL', '绘灵法师', 'pictomancer', NULL, 21, 1),
|
||||
(2041700000000300722, 'FFLOGS_JOB_LABEL', '青魔法师', 'bluemage', NULL, 22, 1),
|
||||
(2041700000000300801, 'FFLOGS_METRIC_LABEL', 'DPS', 'dps', NULL, 1, 1),
|
||||
(2041700000000300802, 'FFLOGS_METRIC_LABEL', 'HPS', 'hps', NULL, 2, 1),
|
||||
(2041700000000300803, 'FFLOGS_METRIC_LABEL', 'rDPS', 'rdps', NULL, 3, 1),
|
||||
(2041700000000300804, 'FFLOGS_METRIC_LABEL', 'aDPS', 'adps', NULL, 4, 1),
|
||||
(2041700000000300805, 'FFLOGS_METRIC_LABEL', 'nDPS', 'ndps', NULL, 5, 1),
|
||||
(2041700000000300806, 'FFLOGS_METRIC_LABEL', 'Boss DPS', 'bossdps', NULL, 6, 1),
|
||||
(2041700000000300807, 'FFLOGS_METRIC_LABEL', 'Boss rDPS', 'bossrdps', NULL, 7, 1),
|
||||
(2041700000000300901, 'FFLOGS_ROLE_LABEL', '坦克', 'tank', NULL, 1, 1),
|
||||
(2041700000000300902, 'FFLOGS_ROLE_LABEL', '治疗', 'healer', NULL, 2, 1),
|
||||
(2041700000000300903, 'FFLOGS_ROLE_LABEL', '输出', 'dps', NULL, 3, 1),
|
||||
(2041700000000301001, 'FFLOGS_SERVER_REGION_LABEL', '国服', 'cn', NULL, 1, 1),
|
||||
(2041700000000301002, 'FFLOGS_SERVER_REGION_LABEL', '日服', 'jp', NULL, 2, 1),
|
||||
(2041700000000301003, 'FFLOGS_SERVER_REGION_LABEL', '美服', 'na', NULL, 3, 1),
|
||||
(2041700000000301004, 'FFLOGS_SERVER_REGION_LABEL', '欧服', 'eu', NULL, 4, 1),
|
||||
(2041700000000301005, 'FFLOGS_SERVER_REGION_LABEL', '韩服', 'kr', NULL, 5, 1),
|
||||
(2041700000000301006, 'FFLOGS_SERVER_REGION_LABEL', '台服', 'tw', NULL, 6, 1),
|
||||
(2041700000000301007, 'FFLOGS_SERVER_REGION_LABEL', '澳服', 'oc', NULL, 7, 1)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`label` = VALUES(`label`),
|
||||
`children_code` = VALUES(`children_code`),
|
||||
`sort` = VALUES(`sort`),
|
||||
`status` = VALUES(`status`),
|
||||
`is_deleted` = 0;
|
||||
|
||||
INSERT INTO `admin_dict` (`id`, `dict_code`, `label`, `value`, `children_code`, `sort`, `status`)
|
||||
VALUES
|
||||
(2041700000000301101, 'FF14_MARKET_REGION', '中国', '中国', NULL, 1, 1),
|
||||
(2041700000000301201, 'FF14_MARKET_DATA_CENTER', '陆行鸟', '陆行鸟', '中国', 1, 1),
|
||||
(2041700000000301202, 'FF14_MARKET_DATA_CENTER', '莫古力', '莫古力', '中国', 2, 1),
|
||||
(2041700000000301203, 'FF14_MARKET_DATA_CENTER', '猫小胖', '猫小胖', '中国', 3, 1),
|
||||
(2041700000000301204, 'FF14_MARKET_DATA_CENTER', '豆豆柴', '豆豆柴', '中国', 4, 1),
|
||||
(2041700000000301301, 'FF14_MARKET_WORLD', '红玉海', '红玉海', '陆行鸟', 1, 1),
|
||||
(2041700000000301302, 'FF14_MARKET_WORLD', '神意之地', '神意之地', '陆行鸟', 2, 1),
|
||||
(2041700000000301303, 'FF14_MARKET_WORLD', '拉诺西亚', '拉诺西亚', '陆行鸟', 3, 1),
|
||||
(2041700000000301304, 'FF14_MARKET_WORLD', '幻影群岛', '幻影群岛', '陆行鸟', 4, 1),
|
||||
(2041700000000301305, 'FF14_MARKET_WORLD', '萌芽池', '萌芽池', '陆行鸟', 5, 1),
|
||||
(2041700000000301306, 'FF14_MARKET_WORLD', '宇宙和音', '宇宙和音', '陆行鸟', 6, 1),
|
||||
(2041700000000301307, 'FF14_MARKET_WORLD', '沃仙曦染', '沃仙曦染', '陆行鸟', 7, 1),
|
||||
(2041700000000301308, 'FF14_MARKET_WORLD', '晨曦王座', '晨曦王座', '陆行鸟', 8, 1),
|
||||
(2041700000000301309, 'FF14_MARKET_WORLD', '白银乡', '白银乡', '莫古力', 9, 1),
|
||||
(2041700000000301310, 'FF14_MARKET_WORLD', '白金幻象', '白金幻象', '莫古力', 10, 1),
|
||||
(2041700000000301311, 'FF14_MARKET_WORLD', '神拳痕', '神拳痕', '莫古力', 11, 1),
|
||||
(2041700000000301312, 'FF14_MARKET_WORLD', '潮风亭', '潮风亭', '莫古力', 12, 1),
|
||||
(2041700000000301313, 'FF14_MARKET_WORLD', '旅人栈桥', '旅人栈桥', '莫古力', 13, 1),
|
||||
(2041700000000301314, 'FF14_MARKET_WORLD', '拂晓之间', '拂晓之间', '莫古力', 14, 1),
|
||||
(2041700000000301315, 'FF14_MARKET_WORLD', '龙巢神殿', '龙巢神殿', '莫古力', 15, 1),
|
||||
(2041700000000301316, 'FF14_MARKET_WORLD', '梦羽宝境', '梦羽宝境', '莫古力', 16, 1),
|
||||
(2041700000000301317, 'FF14_MARKET_WORLD', '紫水栈桥', '紫水栈桥', '猫小胖', 17, 1),
|
||||
(2041700000000301318, 'FF14_MARKET_WORLD', '延夏', '延夏', '猫小胖', 18, 1),
|
||||
(2041700000000301319, 'FF14_MARKET_WORLD', '静语庄园', '静语庄园', '猫小胖', 19, 1),
|
||||
(2041700000000301320, 'FF14_MARKET_WORLD', '摩杜纳', '摩杜纳', '猫小胖', 20, 1),
|
||||
(2041700000000301321, 'FF14_MARKET_WORLD', '海猫茶屋', '海猫茶屋', '猫小胖', 21, 1),
|
||||
(2041700000000301322, 'FF14_MARKET_WORLD', '柔风海湾', '柔风海湾', '猫小胖', 22, 1),
|
||||
(2041700000000301323, 'FF14_MARKET_WORLD', '琥珀原', '琥珀原', '猫小胖', 23, 1),
|
||||
(2041700000000301324, 'FF14_MARKET_WORLD', '水晶塔', '水晶塔', '豆豆柴', 24, 1),
|
||||
(2041700000000301325, 'FF14_MARKET_WORLD', '银泪湖', '银泪湖', '豆豆柴', 25, 1),
|
||||
(2041700000000301326, 'FF14_MARKET_WORLD', '太阳海岸', '太阳海岸', '豆豆柴', 26, 1),
|
||||
(2041700000000301327, 'FF14_MARKET_WORLD', '伊修加德', '伊修加德', '豆豆柴', 27, 1),
|
||||
(2041700000000301328, 'FF14_MARKET_WORLD', '红茶川', '红茶川', '豆豆柴', 28, 1)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`label` = VALUES(`label`),
|
||||
`children_code` = VALUES(`children_code`),
|
||||
`sort` = VALUES(`sort`),
|
||||
`status` = VALUES(`status`),
|
||||
`is_deleted` = 0;
|
||||
|
||||
INSERT INTO `admin_menu` (`id`, `pid`, `name`, `path`, `component`, `redirect`, `auth_code`, `type`, `meta`, `status`, `sort`)
|
||||
VALUES
|
||||
(2041700000000100400, 0, 'QqBot', '/qqbot', NULL, '/qqbot/dashboard', NULL, 'catalog', '{"icon":"lucide:bot","order":110,"title":"QQBot 管理"}', 1, 110),
|
||||
|
||||
@ -6,6 +6,12 @@ import { AdminDict } from './admin-dict.entity';
|
||||
|
||||
const COMPONENT_TYPE_DICT_KEY = 'COMPONENT_TYPE';
|
||||
|
||||
export type AdminDictItem = {
|
||||
childrenCode?: string | null;
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class DictService implements OnApplicationBootstrap {
|
||||
constructor(
|
||||
@ -18,6 +24,15 @@ export class DictService implements OnApplicationBootstrap {
|
||||
}
|
||||
|
||||
async getDictByKey(dictKey: string): Promise<Dict[]> {
|
||||
const list = await this.getDictItemsByKey(dictKey);
|
||||
|
||||
return list.map(({ label, value }) => ({
|
||||
label,
|
||||
value: Number.isNaN(Number(value)) ? value : Number(value),
|
||||
}));
|
||||
}
|
||||
|
||||
async getDictItemsByKey(dictKey: string): Promise<AdminDictItem[]> {
|
||||
const list = await this.dictRepository.find({
|
||||
where: {
|
||||
dictCode: dictKey,
|
||||
@ -30,9 +45,10 @@ export class DictService implements OnApplicationBootstrap {
|
||||
},
|
||||
});
|
||||
|
||||
return list.map(({ label, value }) => ({
|
||||
return list.map(({ childrenCode, label, value }) => ({
|
||||
childrenCode,
|
||||
label,
|
||||
value: Number.isNaN(Number(value)) ? value : Number(value),
|
||||
value,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ export class QqbotCommandEngineService {
|
||||
async handleMessage(message: QqbotNormalizedMessage) {
|
||||
const commands = await this.commandService.listEnabledForMessage(message);
|
||||
for (const command of commands) {
|
||||
const matched = this.commandParser.match(command, message);
|
||||
const matched = await this.commandParser.match(command, message);
|
||||
if (!matched) continue;
|
||||
if (this.commandService.isInCooldown(command)) return true;
|
||||
|
||||
@ -61,7 +61,8 @@ export class QqbotCommandEngineService {
|
||||
status: 'success',
|
||||
});
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : '命令执行失败';
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : '命令执行失败';
|
||||
await this.commandService.logExecution({
|
||||
command,
|
||||
errorMessage,
|
||||
@ -82,7 +83,7 @@ export class QqbotCommandEngineService {
|
||||
const command = body.commandId
|
||||
? await this.commandService.findById(body.commandId)
|
||||
: await this.findMatchedCommand(message);
|
||||
const matched = this.commandParser.match(command, message);
|
||||
const matched = await this.commandParser.match(command, message);
|
||||
if (!matched) {
|
||||
return {
|
||||
matched: false,
|
||||
@ -127,13 +128,12 @@ export class QqbotCommandEngineService {
|
||||
|
||||
private async findMatchedCommand(message: QqbotNormalizedMessage) {
|
||||
const commands = await this.commandService.listEnabledForMessage(message);
|
||||
const command = commands.find((item) =>
|
||||
this.commandParser.match(item, message),
|
||||
);
|
||||
if (!command) {
|
||||
throw new Error('未匹配到命令');
|
||||
for (const command of commands) {
|
||||
if (await this.commandParser.match(command, message)) {
|
||||
return command;
|
||||
}
|
||||
}
|
||||
return command;
|
||||
throw new Error('未匹配到命令');
|
||||
}
|
||||
|
||||
private buildReplyText(
|
||||
@ -202,7 +202,9 @@ export class QqbotCommandEngineService {
|
||||
);
|
||||
}
|
||||
|
||||
private buildPreviewMessage(body: QqbotCommandTestDto): QqbotNormalizedMessage {
|
||||
private buildPreviewMessage(
|
||||
body: QqbotCommandTestDto,
|
||||
): QqbotNormalizedMessage {
|
||||
const targetType = body.targetType || 'private';
|
||||
const targetId = body.targetId || body.userId || '10000';
|
||||
const userId = body.userId || targetId;
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DictService } from '../../admin/dict/dict.service';
|
||||
import type { QqbotCommand } from './qqbot-command.entity';
|
||||
import type { QqbotNormalizedMessage } from '../qqbot.types';
|
||||
import {
|
||||
buildQqbotFf14MarketCatalog,
|
||||
QQBOT_FF14_MARKET_DICT_CODES,
|
||||
type QqbotFf14MarketCatalog,
|
||||
isQqbotFf14DataCenterName,
|
||||
isQqbotFf14LocationName,
|
||||
isQqbotFf14RegionName,
|
||||
@ -18,7 +22,9 @@ export type QqbotCommandMatchResult = {
|
||||
|
||||
@Injectable()
|
||||
export class QqbotCommandParserService {
|
||||
match(command: QqbotCommand, message: QqbotNormalizedMessage) {
|
||||
constructor(private readonly dictService: DictService) {}
|
||||
|
||||
async match(command: QqbotCommand, message: QqbotNormalizedMessage) {
|
||||
const source = `${message.messageText || ''}`.trim();
|
||||
if (!source) return null;
|
||||
|
||||
@ -31,7 +37,7 @@ export class QqbotCommandParserService {
|
||||
if (rawArgs === null) continue;
|
||||
return {
|
||||
alias,
|
||||
input: this.parseInput(command, rawArgs),
|
||||
input: await this.parseInput(command, rawArgs),
|
||||
matched: true,
|
||||
rawArgs,
|
||||
} satisfies QqbotCommandMatchResult;
|
||||
@ -57,7 +63,7 @@ export class QqbotCommandParserService {
|
||||
return null;
|
||||
}
|
||||
|
||||
private parseInput(command: QqbotCommand, rawArgs: string) {
|
||||
private async parseInput(command: QqbotCommand, rawArgs: string) {
|
||||
if (command.parserKey === 'ff14Price') {
|
||||
return this.parseFf14PriceInput(rawArgs);
|
||||
}
|
||||
@ -72,7 +78,8 @@ export class QqbotCommandParserService {
|
||||
};
|
||||
}
|
||||
|
||||
private parseFf14PriceInput(rawArgs: string) {
|
||||
private async parseFf14PriceInput(rawArgs: string) {
|
||||
const catalog = await this.getFf14MarketCatalog();
|
||||
const tokens = rawArgs.split(/\s+/).filter(Boolean);
|
||||
const flags = new Map<string, string | true>();
|
||||
const positional: string[] = [];
|
||||
@ -113,7 +120,7 @@ export class QqbotCommandParserService {
|
||||
}
|
||||
|
||||
if (!world && !dataCenter && positional.length > 1) {
|
||||
const picked = this.pickTrailingFf14Location(positional);
|
||||
const picked = this.pickTrailingFf14Location(catalog, positional);
|
||||
if (picked) {
|
||||
dataCenter = picked.dataCenter || dataCenter;
|
||||
item = picked.item;
|
||||
@ -216,9 +223,12 @@ export class QqbotCommandParserService {
|
||||
};
|
||||
}
|
||||
|
||||
private pickTrailingFf14Location(positional: string[]) {
|
||||
private pickTrailingFf14Location(
|
||||
catalog: QqbotFf14MarketCatalog,
|
||||
positional: string[],
|
||||
) {
|
||||
const last = positional[positional.length - 1];
|
||||
if (!isQqbotFf14LocationName(last)) return null;
|
||||
if (!isQqbotFf14LocationName(catalog, last)) return null;
|
||||
|
||||
const path = splitQqbotFf14WorldPath(last);
|
||||
if (path.dataCenter && path.world) {
|
||||
@ -234,10 +244,11 @@ export class QqbotCommandParserService {
|
||||
const beforePrevious = positional[positional.length - 3];
|
||||
if (
|
||||
previous &&
|
||||
isQqbotFf14DataCenterName(previous) &&
|
||||
isQqbotFf14WorldName(last)
|
||||
isQqbotFf14DataCenterName(catalog, previous) &&
|
||||
isQqbotFf14WorldName(catalog, last)
|
||||
) {
|
||||
const hasRegion = beforePrevious && isQqbotFf14RegionName(beforePrevious);
|
||||
const hasRegion =
|
||||
beforePrevious && isQqbotFf14RegionName(catalog, beforePrevious);
|
||||
return {
|
||||
dataCenter: previous,
|
||||
item: positional.slice(0, hasRegion ? -3 : -2).join(' '),
|
||||
@ -248,8 +259,8 @@ export class QqbotCommandParserService {
|
||||
|
||||
if (
|
||||
previous &&
|
||||
isQqbotFf14RegionName(previous) &&
|
||||
isQqbotFf14DataCenterName(last)
|
||||
isQqbotFf14RegionName(catalog, previous) &&
|
||||
isQqbotFf14DataCenterName(catalog, last)
|
||||
) {
|
||||
return {
|
||||
dataCenter: last,
|
||||
@ -264,6 +275,21 @@ export class QqbotCommandParserService {
|
||||
};
|
||||
}
|
||||
|
||||
private async getFf14MarketCatalog() {
|
||||
const [regions, dataCenters, worlds] = await Promise.all([
|
||||
this.dictService.getDictItemsByKey(QQBOT_FF14_MARKET_DICT_CODES.region),
|
||||
this.dictService.getDictItemsByKey(
|
||||
QQBOT_FF14_MARKET_DICT_CODES.dataCenter,
|
||||
),
|
||||
this.dictService.getDictItemsByKey(QQBOT_FF14_MARKET_DICT_CODES.world),
|
||||
]);
|
||||
return buildQqbotFf14MarketCatalog({
|
||||
dataCenters,
|
||||
regions,
|
||||
worlds,
|
||||
});
|
||||
}
|
||||
|
||||
private normalizeHq(value?: string | true) {
|
||||
if (value === undefined) return undefined;
|
||||
if (value === true) return true;
|
||||
|
||||
@ -2,7 +2,12 @@ import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as http from 'node:http';
|
||||
import * as https from 'node:https';
|
||||
import { resolveQqbotFf14MarketTarget } from './qqbot-ff14-worlds';
|
||||
import { DictService } from '../../../admin/dict/dict.service';
|
||||
import {
|
||||
buildQqbotFf14MarketCatalog,
|
||||
QQBOT_FF14_MARKET_DICT_CODES,
|
||||
resolveQqbotFf14MarketTarget,
|
||||
} from './qqbot-ff14-worlds';
|
||||
|
||||
type HttpMethod = 'GET';
|
||||
|
||||
@ -67,7 +72,10 @@ export class QqbotFf14ClientService {
|
||||
private readonly xivapiChsBaseUrl: string;
|
||||
private readonly universalisBaseUrl: string;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly dictService: DictService,
|
||||
) {
|
||||
this.xivapiBaseUrl =
|
||||
this.configService.get<string>('FF14_XIVAPI_BASE_URL') ||
|
||||
'https://v2.xivapi.com/api';
|
||||
@ -114,7 +122,7 @@ export class QqbotFf14ClientService {
|
||||
region?: string;
|
||||
world?: string;
|
||||
}): Promise<QqbotFf14PriceResult> {
|
||||
const marketTarget = this.resolveMarketTarget(params);
|
||||
const marketTarget = await this.resolveMarketTarget(params);
|
||||
const item = await this.resolveItem(params);
|
||||
if (item.isUntradable) {
|
||||
return {
|
||||
@ -127,9 +135,9 @@ export class QqbotFf14ClientService {
|
||||
}
|
||||
|
||||
const url = new URL(
|
||||
`${this.universalisBaseUrl}/${encodeURIComponent(
|
||||
marketTarget.target,
|
||||
)}/${item.itemId}`,
|
||||
`${this.universalisBaseUrl}/${encodeURIComponent(marketTarget.target)}/${
|
||||
item.itemId
|
||||
}`,
|
||||
);
|
||||
url.searchParams.set('entries', '10');
|
||||
url.searchParams.set('listings', '10');
|
||||
@ -211,7 +219,9 @@ export class QqbotFf14ClientService {
|
||||
const total = item.total || price * quantity;
|
||||
const retainerName = item.retainerName || '未知雇员';
|
||||
const worldName = item.worldName || result.world;
|
||||
return `[${hq}]${this.formatPrice(price)} x ${quantity} = ${this.formatPrice(
|
||||
return `[${hq}]${this.formatPrice(
|
||||
price,
|
||||
)} x ${quantity} = ${this.formatPrice(
|
||||
total,
|
||||
)} ${retainerName} (${worldName})`;
|
||||
})
|
||||
@ -255,25 +265,45 @@ export class QqbotFf14ClientService {
|
||||
return Math.round(value).toLocaleString('en-US');
|
||||
}
|
||||
|
||||
private normalizeWorld(world?: string) {
|
||||
const raw =
|
||||
`${world || this.configService.get<string>('FF14_DEFAULT_WORLD') || '中国'}`.trim();
|
||||
private normalizeWorld(world?: string, fallback?: string) {
|
||||
const raw = `${
|
||||
world ||
|
||||
this.configService.get<string>('FF14_DEFAULT_WORLD') ||
|
||||
fallback ||
|
||||
''
|
||||
}`.trim();
|
||||
return raw;
|
||||
}
|
||||
|
||||
private resolveMarketTarget(params: {
|
||||
private async resolveMarketTarget(params: {
|
||||
dataCenter?: string;
|
||||
region?: string;
|
||||
world?: string;
|
||||
}) {
|
||||
return resolveQqbotFf14MarketTarget({
|
||||
const catalog = await this.getFf14MarketCatalog();
|
||||
return resolveQqbotFf14MarketTarget(catalog, {
|
||||
dataCenter: params.dataCenter,
|
||||
fallback: this.normalizeWorld(params.world),
|
||||
fallback: this.normalizeWorld(params.world, catalog.defaultRegion),
|
||||
region: params.region,
|
||||
world: params.world,
|
||||
});
|
||||
}
|
||||
|
||||
private async getFf14MarketCatalog() {
|
||||
const [regions, dataCenters, worlds] = await Promise.all([
|
||||
this.dictService.getDictItemsByKey(QQBOT_FF14_MARKET_DICT_CODES.region),
|
||||
this.dictService.getDictItemsByKey(
|
||||
QQBOT_FF14_MARKET_DICT_CODES.dataCenter,
|
||||
),
|
||||
this.dictService.getDictItemsByKey(QQBOT_FF14_MARKET_DICT_CODES.world),
|
||||
]);
|
||||
return buildQqbotFf14MarketCatalog({
|
||||
dataCenters,
|
||||
regions,
|
||||
worlds,
|
||||
});
|
||||
}
|
||||
|
||||
private normalizeXivapiLanguage(language?: string) {
|
||||
const value = `${language || 'chs'}`.trim().toLowerCase();
|
||||
if (['zh', 'zh-cn', 'zh_hans', 'cn', 'chs'].includes(value)) return 'chs';
|
||||
@ -328,8 +358,7 @@ export class QqbotFf14ClientService {
|
||||
'XIVAPI 物品解析',
|
||||
);
|
||||
return (data.results || []).filter(
|
||||
(result) =>
|
||||
result.sheet === 'Item' || result.fields?.Name || result.name,
|
||||
(result) => result.sheet === 'Item' || result.fields?.Name || result.name,
|
||||
);
|
||||
}
|
||||
|
||||
@ -400,9 +429,7 @@ export class QqbotFf14ClientService {
|
||||
});
|
||||
response.on('end', () => {
|
||||
if ((response.statusCode || 500) >= 400) {
|
||||
reject(
|
||||
new Error(`${context}失败:${response.statusCode}`),
|
||||
);
|
||||
reject(new Error(`${context}失败:${response.statusCode}`));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
||||
37
src/qqbot/plugins/ff14Market/qqbot-ff14-worlds.spec.ts
Normal file
37
src/qqbot/plugins/ff14Market/qqbot-ff14-worlds.spec.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import {
|
||||
buildQqbotFf14MarketCatalog,
|
||||
resolveQqbotFf14MarketTarget,
|
||||
} from './qqbot-ff14-worlds';
|
||||
|
||||
describe('qqbot ff14 market worlds', () => {
|
||||
const catalog = buildQqbotFf14MarketCatalog({
|
||||
dataCenters: [
|
||||
{ childrenCode: '中国', label: '猫小胖', value: '猫小胖' },
|
||||
{ childrenCode: '中国', label: '陆行鸟', value: '陆行鸟' },
|
||||
],
|
||||
regions: [{ label: '中国', value: '中国' }],
|
||||
worlds: [
|
||||
{ childrenCode: '猫小胖', label: '琥珀原', value: '琥珀原' },
|
||||
{ childrenCode: '陆行鸟', label: '红玉海', value: '红玉海' },
|
||||
],
|
||||
});
|
||||
|
||||
it('resolves world labels from dict catalog', () => {
|
||||
expect(resolveQqbotFf14MarketTarget(catalog, { world: '琥珀原' })).toEqual({
|
||||
dataCenter: '猫小胖',
|
||||
label: '中国 / 猫小胖 / 琥珀原',
|
||||
region: '中国',
|
||||
target: '琥珀原',
|
||||
world: '琥珀原',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects world and data center mismatch from dict catalog', () => {
|
||||
expect(() =>
|
||||
resolveQqbotFf14MarketTarget(catalog, {
|
||||
dataCenter: '陆行鸟',
|
||||
world: '琥珀原',
|
||||
}),
|
||||
).toThrow('服务器 琥珀原 不属于大区 陆行鸟');
|
||||
});
|
||||
});
|
||||
@ -1,9 +1,17 @@
|
||||
import type { AdminDictItem } from '../../../admin/dict/dict.service';
|
||||
|
||||
export type QqbotFf14DataCenter = {
|
||||
name: string;
|
||||
region: string;
|
||||
worlds: string[];
|
||||
};
|
||||
|
||||
export type QqbotFf14MarketCatalog = {
|
||||
dataCenters: QqbotFf14DataCenter[];
|
||||
defaultRegion?: string;
|
||||
regions: string[];
|
||||
};
|
||||
|
||||
export type QqbotFf14MarketTarget = {
|
||||
dataCenter?: string;
|
||||
label: string;
|
||||
@ -12,80 +20,76 @@ export type QqbotFf14MarketTarget = {
|
||||
world?: string;
|
||||
};
|
||||
|
||||
export const QQBOT_FF14_DEFAULT_REGION = '中国';
|
||||
export const QQBOT_FF14_MARKET_DICT_CODES = {
|
||||
dataCenter: 'FF14_MARKET_DATA_CENTER',
|
||||
region: 'FF14_MARKET_REGION',
|
||||
world: 'FF14_MARKET_WORLD',
|
||||
};
|
||||
|
||||
export const QQBOT_FF14_CHINA_DATA_CENTERS: QqbotFf14DataCenter[] = [
|
||||
{
|
||||
name: '陆行鸟',
|
||||
region: QQBOT_FF14_DEFAULT_REGION,
|
||||
worlds: [
|
||||
'红玉海',
|
||||
'神意之地',
|
||||
'拉诺西亚',
|
||||
'幻影群岛',
|
||||
'萌芽池',
|
||||
'宇宙和音',
|
||||
'沃仙曦染',
|
||||
'晨曦王座',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '莫古力',
|
||||
region: QQBOT_FF14_DEFAULT_REGION,
|
||||
worlds: [
|
||||
'白银乡',
|
||||
'白金幻象',
|
||||
'神拳痕',
|
||||
'潮风亭',
|
||||
'旅人栈桥',
|
||||
'拂晓之间',
|
||||
'龙巢神殿',
|
||||
'梦羽宝境',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '猫小胖',
|
||||
region: QQBOT_FF14_DEFAULT_REGION,
|
||||
worlds: [
|
||||
'紫水栈桥',
|
||||
'延夏',
|
||||
'静语庄园',
|
||||
'摩杜纳',
|
||||
'海猫茶屋',
|
||||
'柔风海湾',
|
||||
'琥珀原',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '豆豆柴',
|
||||
region: QQBOT_FF14_DEFAULT_REGION,
|
||||
worlds: ['水晶塔', '银泪湖', '太阳海岸', '伊修加德', '红茶川'],
|
||||
},
|
||||
];
|
||||
export function buildQqbotFf14MarketCatalog(input: {
|
||||
dataCenters: AdminDictItem[];
|
||||
regions: AdminDictItem[];
|
||||
worlds: AdminDictItem[];
|
||||
}): QqbotFf14MarketCatalog {
|
||||
const regions = input.regions.map(getDictDisplayValue).filter(Boolean);
|
||||
const defaultRegion = regions[0];
|
||||
const dataCenters = input.dataCenters
|
||||
.map((item) => {
|
||||
const name = getDictDisplayValue(item);
|
||||
if (!name) return null;
|
||||
return {
|
||||
name,
|
||||
region:
|
||||
normalizeQqbotFf14WorldValue(item.childrenCode) || defaultRegion,
|
||||
worlds: input.worlds
|
||||
.filter(({ childrenCode }) => childrenCode === getDictRawValue(item))
|
||||
.map(getDictDisplayValue)
|
||||
.filter(Boolean),
|
||||
};
|
||||
})
|
||||
.filter((item): item is QqbotFf14DataCenter => !!item);
|
||||
|
||||
export function isQqbotFf14DataCenterName(value?: string) {
|
||||
return {
|
||||
dataCenters,
|
||||
defaultRegion,
|
||||
regions,
|
||||
};
|
||||
}
|
||||
|
||||
export function isQqbotFf14DataCenterName(
|
||||
catalog: QqbotFf14MarketCatalog,
|
||||
value?: string,
|
||||
) {
|
||||
const name = normalizeQqbotFf14WorldValue(value);
|
||||
return QQBOT_FF14_CHINA_DATA_CENTERS.some((item) => item.name === name);
|
||||
return catalog.dataCenters.some((item) => item.name === name);
|
||||
}
|
||||
|
||||
export function isQqbotFf14RegionName(value?: string) {
|
||||
return normalizeQqbotFf14WorldValue(value) === QQBOT_FF14_DEFAULT_REGION;
|
||||
}
|
||||
|
||||
export function isQqbotFf14WorldName(value?: string) {
|
||||
export function isQqbotFf14RegionName(
|
||||
catalog: QqbotFf14MarketCatalog,
|
||||
value?: string,
|
||||
) {
|
||||
const name = normalizeQqbotFf14WorldValue(value);
|
||||
return QQBOT_FF14_CHINA_DATA_CENTERS.some((item) =>
|
||||
item.worlds.includes(name),
|
||||
);
|
||||
return catalog.regions.includes(name);
|
||||
}
|
||||
|
||||
export function isQqbotFf14LocationName(value?: string) {
|
||||
export function isQqbotFf14WorldName(
|
||||
catalog: QqbotFf14MarketCatalog,
|
||||
value?: string,
|
||||
) {
|
||||
const name = normalizeQqbotFf14WorldValue(value);
|
||||
return catalog.dataCenters.some((item) => item.worlds.includes(name));
|
||||
}
|
||||
|
||||
export function isQqbotFf14LocationName(
|
||||
catalog: QqbotFf14MarketCatalog,
|
||||
value?: string,
|
||||
) {
|
||||
const name = normalizeQqbotFf14WorldValue(value);
|
||||
const path = splitQqbotFf14WorldPath(name);
|
||||
return (
|
||||
isQqbotFf14RegionName(name) ||
|
||||
isQqbotFf14DataCenterName(name) ||
|
||||
isQqbotFf14WorldName(name) ||
|
||||
isQqbotFf14RegionName(catalog, name) ||
|
||||
isQqbotFf14DataCenterName(catalog, name) ||
|
||||
isQqbotFf14WorldName(catalog, name) ||
|
||||
(!!path.dataCenter && !!path.world)
|
||||
);
|
||||
}
|
||||
@ -114,19 +118,24 @@ export function splitQqbotFf14WorldPath(value?: string) {
|
||||
};
|
||||
}
|
||||
|
||||
export function findQqbotFf14DataCenterByWorld(world?: string) {
|
||||
export function findQqbotFf14DataCenterByWorld(
|
||||
catalog: QqbotFf14MarketCatalog,
|
||||
world?: string,
|
||||
) {
|
||||
const worldName = normalizeQqbotFf14WorldValue(world);
|
||||
return QQBOT_FF14_CHINA_DATA_CENTERS.find((item) =>
|
||||
item.worlds.includes(worldName),
|
||||
);
|
||||
return catalog.dataCenters.find((item) => item.worlds.includes(worldName));
|
||||
}
|
||||
|
||||
export function resolveQqbotFf14MarketTarget(params: {
|
||||
dataCenter?: string;
|
||||
fallback?: string;
|
||||
region?: string;
|
||||
world?: string;
|
||||
}): QqbotFf14MarketTarget {
|
||||
export function resolveQqbotFf14MarketTarget(
|
||||
catalog: QqbotFf14MarketCatalog,
|
||||
params: {
|
||||
dataCenter?: string;
|
||||
fallback?: string;
|
||||
region?: string;
|
||||
world?: string;
|
||||
},
|
||||
): QqbotFf14MarketTarget {
|
||||
const defaultRegion = catalog.defaultRegion || '';
|
||||
const fallback = normalizeQqbotFf14WorldValue(params.fallback);
|
||||
const path = splitQqbotFf14WorldPath(params.world);
|
||||
const region = normalizeQqbotFf14WorldValue(params.region || path.region);
|
||||
@ -134,10 +143,8 @@ export function resolveQqbotFf14MarketTarget(params: {
|
||||
params.dataCenter || path.dataCenter,
|
||||
);
|
||||
const rawWorld = normalizeQqbotFf14WorldValue(path.world || params.world);
|
||||
const world =
|
||||
dataCenter && rawWorld === QQBOT_FF14_DEFAULT_REGION ? '' : rawWorld;
|
||||
const raw =
|
||||
world || dataCenter || region || fallback || QQBOT_FF14_DEFAULT_REGION;
|
||||
const world = dataCenter && rawWorld === defaultRegion ? '' : rawWorld;
|
||||
const raw = world || dataCenter || region || fallback || defaultRegion;
|
||||
|
||||
if (region && dataCenter && (!world || world === region)) {
|
||||
return {
|
||||
@ -148,16 +155,16 @@ export function resolveQqbotFf14MarketTarget(params: {
|
||||
};
|
||||
}
|
||||
|
||||
if (raw === QQBOT_FF14_DEFAULT_REGION) {
|
||||
if (raw && raw === defaultRegion) {
|
||||
return {
|
||||
label: QQBOT_FF14_DEFAULT_REGION,
|
||||
region: QQBOT_FF14_DEFAULT_REGION,
|
||||
target: QQBOT_FF14_DEFAULT_REGION,
|
||||
label: defaultRegion,
|
||||
region: defaultRegion,
|
||||
target: defaultRegion,
|
||||
};
|
||||
}
|
||||
|
||||
if (dataCenter && world && world !== dataCenter) {
|
||||
const matchedDataCenter = QQBOT_FF14_CHINA_DATA_CENTERS.find(
|
||||
const matchedDataCenter = catalog.dataCenters.find(
|
||||
(item) => item.name === dataCenter,
|
||||
);
|
||||
if (matchedDataCenter && !matchedDataCenter.worlds.includes(world)) {
|
||||
@ -174,7 +181,7 @@ export function resolveQqbotFf14MarketTarget(params: {
|
||||
};
|
||||
}
|
||||
|
||||
const matchedWorldDataCenter = findQqbotFf14DataCenterByWorld(raw);
|
||||
const matchedWorldDataCenter = findQqbotFf14DataCenterByWorld(catalog, raw);
|
||||
if (matchedWorldDataCenter) {
|
||||
return {
|
||||
dataCenter: matchedWorldDataCenter.name,
|
||||
@ -185,11 +192,11 @@ export function resolveQqbotFf14MarketTarget(params: {
|
||||
};
|
||||
}
|
||||
|
||||
if (isQqbotFf14DataCenterName(raw)) {
|
||||
if (isQqbotFf14DataCenterName(catalog, raw)) {
|
||||
return {
|
||||
dataCenter: raw,
|
||||
label: `${QQBOT_FF14_DEFAULT_REGION} / ${raw}`,
|
||||
region: QQBOT_FF14_DEFAULT_REGION,
|
||||
label: defaultRegion ? `${defaultRegion} / ${raw}` : raw,
|
||||
region: defaultRegion,
|
||||
target: raw,
|
||||
};
|
||||
}
|
||||
@ -200,6 +207,14 @@ export function resolveQqbotFf14MarketTarget(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeQqbotFf14WorldValue(value?: string) {
|
||||
function getDictDisplayValue(item: AdminDictItem) {
|
||||
return normalizeQqbotFf14WorldValue(item.label || item.value);
|
||||
}
|
||||
|
||||
function getDictRawValue(item: AdminDictItem) {
|
||||
return normalizeQqbotFf14WorldValue(item.value || item.label);
|
||||
}
|
||||
|
||||
function normalizeQqbotFf14WorldValue(value?: string | null) {
|
||||
return `${value || ''}`.trim();
|
||||
}
|
||||
|
||||
69
src/qqbot/plugins/fflogs/qqbot-fflogs-client.service.spec.ts
Normal file
69
src/qqbot/plugins/fflogs/qqbot-fflogs-client.service.spec.ts
Normal file
@ -0,0 +1,69 @@
|
||||
jest.mock(
|
||||
'@/common',
|
||||
() => ({
|
||||
ensureSnowflakeId: jest.fn(),
|
||||
setDictDecodeCache: jest.fn(),
|
||||
}),
|
||||
{ virtual: true },
|
||||
);
|
||||
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { DictService } from '../../../admin/dict/dict.service';
|
||||
import { QqbotFflogsClientService } from './qqbot-fflogs-client.service';
|
||||
|
||||
describe('QqbotFflogsClientService', () => {
|
||||
const dicts = {
|
||||
FFLOGS_ENCOUNTER_LABEL: [
|
||||
{ label: 'M9S Vamp Fatale', value: 'vampfatale' },
|
||||
{ label: 'M12S P2 Lindwurm II', value: 'lindwurmii' },
|
||||
],
|
||||
FFLOGS_JOB_LABEL: [{ label: '机工士', value: 'machinist' }],
|
||||
FFLOGS_METRIC_LABEL: [{ label: 'DPS', value: 'dps' }],
|
||||
FFLOGS_ROLE_LABEL: [],
|
||||
FFLOGS_SERVER_REGION_LABEL: [{ label: '国服', value: 'cn' }],
|
||||
};
|
||||
|
||||
const service = new QqbotFflogsClientService(
|
||||
{
|
||||
get: jest.fn(),
|
||||
} as unknown as ConfigService,
|
||||
{
|
||||
getDictByKey: jest.fn(
|
||||
async (dictKey: keyof typeof dicts) => dicts[dictKey] || [],
|
||||
),
|
||||
} as unknown as DictService,
|
||||
);
|
||||
|
||||
it('formats character rankings with dict labels', async () => {
|
||||
const localizationMaps = await (service as any).getLocalizationMaps();
|
||||
const replyText = (service as any).buildReplyText({
|
||||
allStarText: '全明星:318分 / 排名第1857',
|
||||
characterId: 20962075,
|
||||
characterName: 'Kwi柊司',
|
||||
localizationMaps,
|
||||
metric: 'DPS',
|
||||
rankings: [
|
||||
{
|
||||
bestAmount: 36635,
|
||||
encounter: { name: 'Vamp Fatale' },
|
||||
rankPercent: 72.4,
|
||||
spec: 'Machinist',
|
||||
},
|
||||
{
|
||||
bestAmount: 0,
|
||||
encounter: { name: 'Lindwurm II' },
|
||||
rankPercent: 0,
|
||||
},
|
||||
],
|
||||
serverName: '琥珀原',
|
||||
serverRegion: 'CN',
|
||||
url: 'https://cn.fflogs.com/character/cn/example/Kwi',
|
||||
});
|
||||
|
||||
expect(replyText).toContain('FFLogs 战绩:Kwi柊司 @ 琥珀原(国服)');
|
||||
expect(replyText).toContain(
|
||||
'1. M9S Vamp Fatale:72.4% | DPS 36,635 | 机工士',
|
||||
);
|
||||
expect(replyText).toContain('2. M12S P2 Lindwurm II:暂无有效排名');
|
||||
});
|
||||
});
|
||||
@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as http from 'node:http';
|
||||
import * as https from 'node:https';
|
||||
import { DictService } from '../../../admin/dict/dict.service';
|
||||
|
||||
type HttpMethod = 'GET' | 'POST';
|
||||
|
||||
@ -35,6 +36,19 @@ type FflogsCharacterSummaryResponse = {
|
||||
|
||||
type FflogsRankingItem = Record<string, any>;
|
||||
|
||||
const FFLOGS_LOCALIZATION_DICT_CODES = {
|
||||
encounter: 'FFLOGS_ENCOUNTER_LABEL',
|
||||
job: 'FFLOGS_JOB_LABEL',
|
||||
metric: 'FFLOGS_METRIC_LABEL',
|
||||
role: 'FFLOGS_ROLE_LABEL',
|
||||
serverRegion: 'FFLOGS_SERVER_REGION_LABEL',
|
||||
};
|
||||
|
||||
type FflogsLocalizationMaps = Record<
|
||||
keyof typeof FFLOGS_LOCALIZATION_DICT_CODES,
|
||||
Map<string, string>
|
||||
>;
|
||||
|
||||
export type QqbotFflogsCharacterSummaryInput = {
|
||||
character?: string;
|
||||
characterName?: string;
|
||||
@ -74,7 +88,10 @@ export class QqbotFflogsClientService {
|
||||
private readonly tokenUrl: string;
|
||||
private readonly webBaseUrl: string;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly dictService: DictService,
|
||||
) {
|
||||
this.baseUrl = this.normalizeBaseUrl(
|
||||
this.configService.get<string>('FFLOGS_BASE_URL') ||
|
||||
'https://www.fflogs.com',
|
||||
@ -196,6 +213,7 @@ export class QqbotFflogsClientService {
|
||||
character.server?.slug || serverSlug,
|
||||
character.name || characterName,
|
||||
);
|
||||
const localizationMaps = await this.getLocalizationMaps();
|
||||
|
||||
return {
|
||||
allStarText,
|
||||
@ -206,7 +224,9 @@ export class QqbotFflogsClientService {
|
||||
allStarText,
|
||||
characterId: character.id,
|
||||
characterName: character.name || characterName,
|
||||
metric: variables.metric || 'DPS',
|
||||
rankings,
|
||||
localizationMaps,
|
||||
serverName,
|
||||
serverRegion,
|
||||
url,
|
||||
@ -327,29 +347,51 @@ export class QqbotFflogsClientService {
|
||||
allStarText?: string;
|
||||
characterId?: number;
|
||||
characterName: string;
|
||||
localizationMaps: FflogsLocalizationMaps;
|
||||
metric: string;
|
||||
rankings: FflogsRankingItem[];
|
||||
serverName: string;
|
||||
serverRegion: string;
|
||||
url: string;
|
||||
}) {
|
||||
const header = `FFLogs:${params.characterName} - ${params.serverName} (${params.serverRegion})`;
|
||||
const idText = params.characterId ? `角色 ID:${params.characterId}` : '';
|
||||
const region = this.localizeServerRegion(
|
||||
params.serverRegion,
|
||||
params.localizationMaps,
|
||||
);
|
||||
const header = `FFLogs 战绩:${params.characterName} @ ${params.serverName}(${region})`;
|
||||
const idText = params.characterId ? `角色ID:${params.characterId}` : '';
|
||||
const rankingText = params.rankings.length
|
||||
? params.rankings
|
||||
.map((item, index) => this.formatRanking(item, index))
|
||||
.join('\n')
|
||||
: '暂无公开排名数据';
|
||||
? [
|
||||
'公开排名:',
|
||||
...params.rankings.map((item, index) =>
|
||||
this.formatRanking(
|
||||
item,
|
||||
index,
|
||||
params.metric,
|
||||
params.localizationMaps,
|
||||
),
|
||||
),
|
||||
].join('\n')
|
||||
: '公开排名:暂无公开排名数据';
|
||||
return [header, idText, params.allStarText, rankingText, params.url]
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
private formatRanking(item: FflogsRankingItem, index: number) {
|
||||
const encounter = this.pickText(
|
||||
item.encounter?.name,
|
||||
item.encounterName,
|
||||
item.name,
|
||||
`记录 ${index + 1}`,
|
||||
private formatRanking(
|
||||
item: FflogsRankingItem,
|
||||
index: number,
|
||||
fallbackMetric: string,
|
||||
localizationMaps: FflogsLocalizationMaps,
|
||||
) {
|
||||
const encounter = this.localizeEncounter(
|
||||
this.pickText(
|
||||
item.encounter?.name,
|
||||
item.encounterName,
|
||||
item.name,
|
||||
`记录 ${index + 1}`,
|
||||
),
|
||||
localizationMaps,
|
||||
);
|
||||
const percent = this.pickNumber(
|
||||
item.rankPercent,
|
||||
@ -358,16 +400,29 @@ export class QqbotFflogsClientService {
|
||||
item.historicalPercent,
|
||||
);
|
||||
const amount = this.pickNumber(item.bestAmount, item.amount, item.total);
|
||||
const spec = this.pickText(item.spec, item.specName, item.class, item.role);
|
||||
const spec = this.localizeSpec(
|
||||
this.pickText(item.spec, item.specName, item.class, item.role),
|
||||
localizationMaps,
|
||||
);
|
||||
const rank = this.pickText(item.rank, item.regionRank, item.serverRank);
|
||||
|
||||
if (!this.hasMeaningfulRanking(percent, amount)) {
|
||||
return `${index + 1}. ${encounter}:暂无有效排名`;
|
||||
}
|
||||
|
||||
const metric = this.localizeMetric(
|
||||
this.pickText(item.metric, item.metricName, fallbackMetric),
|
||||
localizationMaps,
|
||||
);
|
||||
const parts = [
|
||||
`${index + 1}. ${encounter}`,
|
||||
percent !== undefined ? `${this.formatNumber(percent)}%` : '',
|
||||
amount !== undefined ? this.formatNumber(amount) : '',
|
||||
`${index + 1}. ${encounter}:${
|
||||
percent !== undefined ? `${this.formatNumber(percent)}%` : '百分位暂无'
|
||||
}`,
|
||||
amount !== undefined ? `${metric} ${this.formatNumber(amount)}` : '',
|
||||
spec,
|
||||
rank ? `Rank ${rank}` : '',
|
||||
rank ? this.formatRank(rank) : '',
|
||||
].filter(Boolean);
|
||||
return parts.join(' / ');
|
||||
return parts.join(' | ');
|
||||
}
|
||||
|
||||
private pickRankings(payload: any): FflogsRankingItem[] {
|
||||
@ -399,8 +454,8 @@ export class QqbotFflogsClientService {
|
||||
allStars.serverRank,
|
||||
);
|
||||
const parts = [
|
||||
points !== undefined ? `全明星分:${this.formatNumber(points)}` : '',
|
||||
rank ? `名次:${rank}` : '',
|
||||
points !== undefined ? `全明星:${this.formatNumber(points)}分` : '',
|
||||
rank ? this.formatRank(rank) : '',
|
||||
].filter(Boolean);
|
||||
return parts.length ? parts.join(' / ') : undefined;
|
||||
}
|
||||
@ -495,12 +550,98 @@ export class QqbotFflogsClientService {
|
||||
|
||||
private formatNumber(value: number) {
|
||||
const digits = Math.abs(value) >= 100 ? 0 : 1;
|
||||
return value.toLocaleString('en-US', {
|
||||
return value.toLocaleString('zh-CN', {
|
||||
maximumFractionDigits: digits,
|
||||
minimumFractionDigits: 0,
|
||||
});
|
||||
}
|
||||
|
||||
private formatRank(value: string) {
|
||||
const rank = value.replace(/^#/, '').trim();
|
||||
if (!rank) return '';
|
||||
if (rank.startsWith('第') || rank.endsWith('名')) return `排名${rank}`;
|
||||
return `排名第${rank}`;
|
||||
}
|
||||
|
||||
private hasMeaningfulRanking(percent?: number, amount?: number) {
|
||||
return (
|
||||
(percent !== undefined && percent > 0) ||
|
||||
(amount !== undefined && amount > 0)
|
||||
);
|
||||
}
|
||||
|
||||
private async getLocalizationMaps(): Promise<FflogsLocalizationMaps> {
|
||||
const [encounter, job, metric, role, serverRegion] = await Promise.all([
|
||||
this.getNormalizedDictMap(FFLOGS_LOCALIZATION_DICT_CODES.encounter),
|
||||
this.getNormalizedDictMap(FFLOGS_LOCALIZATION_DICT_CODES.job),
|
||||
this.getNormalizedDictMap(FFLOGS_LOCALIZATION_DICT_CODES.metric),
|
||||
this.getNormalizedDictMap(FFLOGS_LOCALIZATION_DICT_CODES.role),
|
||||
this.getNormalizedDictMap(FFLOGS_LOCALIZATION_DICT_CODES.serverRegion),
|
||||
]);
|
||||
|
||||
return {
|
||||
encounter,
|
||||
job,
|
||||
metric,
|
||||
role,
|
||||
serverRegion,
|
||||
};
|
||||
}
|
||||
|
||||
private async getNormalizedDictMap(dictCode: string) {
|
||||
const dicts = await this.dictService.getDictByKey(dictCode);
|
||||
return new Map(
|
||||
dicts.map(({ label, value }) => [
|
||||
this.normalizeLookupKey(`${value}`),
|
||||
`${label}`,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
private localizeEncounter(
|
||||
value: string,
|
||||
localizationMaps: FflogsLocalizationMaps,
|
||||
) {
|
||||
return (
|
||||
localizationMaps.encounter.get(this.normalizeLookupKey(value)) || value
|
||||
);
|
||||
}
|
||||
|
||||
private localizeMetric(
|
||||
value: string,
|
||||
localizationMaps: FflogsLocalizationMaps,
|
||||
) {
|
||||
return (
|
||||
localizationMaps.metric.get(this.normalizeLookupKey(value)) ||
|
||||
value ||
|
||||
'DPS'
|
||||
);
|
||||
}
|
||||
|
||||
private localizeServerRegion(
|
||||
value: string,
|
||||
localizationMaps: FflogsLocalizationMaps,
|
||||
) {
|
||||
return (
|
||||
localizationMaps.serverRegion.get(this.normalizeLookupKey(value)) ||
|
||||
value.toUpperCase()
|
||||
);
|
||||
}
|
||||
|
||||
private localizeSpec(
|
||||
value: string,
|
||||
localizationMaps: FflogsLocalizationMaps,
|
||||
) {
|
||||
const key = this.normalizeLookupKey(value);
|
||||
return (
|
||||
localizationMaps.job.get(key) || localizationMaps.role.get(key) || value
|
||||
);
|
||||
}
|
||||
|
||||
private normalizeLookupKey(value: string) {
|
||||
return `${value || ''}`.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
}
|
||||
|
||||
private removeUndefined(input: Record<string, any>) {
|
||||
return Object.entries(input).reduce<Record<string, any>>(
|
||||
(result, [key, value]) => {
|
||||
|
||||
@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AdminAuthGuardModule } from '@/admin/auth/admin-auth-guard.module';
|
||||
import { DictModule } from '@/admin/dict/dict.module';
|
||||
import { QqbotAccountController } from './account/qqbot-account.controller';
|
||||
import { QqbotAccountAbility } from './account/qqbot-account-ability.entity';
|
||||
import { QqbotAccount } from './account/qqbot-account.entity';
|
||||
@ -55,6 +56,7 @@ import { QqbotSendService } from './send/qqbot-send.service';
|
||||
imports: [
|
||||
ConfigModule,
|
||||
AdminAuthGuardModule,
|
||||
DictModule,
|
||||
TypeOrmModule.forFeature([
|
||||
QqbotAccount,
|
||||
QqbotAccountAbility,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user