yumi-admin/apps/src/views/operate/tools-other.vue
2026-04-13 19:31:36 +08:00

613 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts" setup>
import type { EchartsUIType } from '@vben/plugins/echarts';
import { computed, reactive, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { getCountryAlls } from '#/api/legacy/system';
import {
alisMethod,
checkUserSwapUser,
clearRedisKey,
executeUserCancelAndRestore,
executeUserSwapUser,
getAccessTokenByUserId,
getUserToken,
requestApiSing,
updateUserRoomRegion,
} from '#/api/legacy/tools';
import {
Button,
Card,
Form,
FormItem,
Input,
Modal,
Radio,
RadioGroup,
Select,
TextArea,
message,
} from 'antdv-next';
defineOptions({ name: 'OperateToolsOther' });
const ORIGIN_PLATFORM_OPTIONS = [
{ label: 'iOS', value: 'iOS' },
{ label: 'Android', value: 'Android' },
{ label: 'H5', value: 'H5' },
];
const ALIAS_FILLS = [
{ name: '鲜花', probability: '0.2' },
{ name: '橘子', probability: '0.15' },
{ name: '苹果', probability: '0.15' },
{ name: '西瓜', probability: '0.1' },
{ name: '香蕉', probability: '0.08' },
{ name: '果汁', probability: '0.05' },
{ name: '手机', probability: '0.01' },
{ name: '电脑', probability: '0.001' },
];
const USER_SWAP_TIPS_MAP: Record<string, { desc: string; type: 'error' | 'warn' }> = {
AUTH_OPENID_NOT_EQUALS: { desc: '注册来源OpenId不一致', type: 'warn' },
ORIGIN_NOT_EQUALS: { desc: '用户来自不同系统', type: 'warn' },
SOURCE_USER_AUTH_IS_NULL: { desc: '原始用户认证信息没找到', type: 'error' },
SOURCE_USER_IS_NULL: { desc: '原始用户没有找到', type: 'error' },
TARGET_USER_AUTH_IS_NULL: { desc: '目标用户认证信息没找到', type: 'error' },
TARGET_USER_IS_NULL: { desc: '目标用户没有找到', type: 'error' },
};
const aliasChartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(aliasChartRef);
const countries = ref<Array<Record<string, any>>>([]);
const apiSignDialogOpen = ref(false);
const apiSignLoading = ref(false);
const apiSignResult = ref('');
const aliasDialogOpen = ref(false);
const aliasLoading = ref(false);
const userSwapCheckLoading = ref(false);
const userSwapExecuteLoading = ref(false);
const userSwapTips = ref('');
const userSwapResults = ref<Array<{ desc: string; type: 'error' | 'warn' }>>([]);
const userSwapBlocked = ref(true);
const tokenLoading = ref(false);
const tokenResult = ref<Record<string, any> | string>({});
const redisLoading = ref(false);
const roomRegionLoading = ref(false);
const cancelRestoreLoading = ref(false);
const apiSignForm = reactive({
body: '',
key: '',
platform: 'iOS',
});
const apiSignAttributes = ref<Array<Record<string, string>>>([
{ key: '', value: '' },
]);
const aliasForm = reactive({
bets: [{ name: '', probability: '' }],
runSize: 10000,
});
const userSwapForm = reactive({
sourceUserId: '',
targetUserId: '',
});
const tokenForm = reactive({
mode: 'TOKEN_PARSE',
token: '',
userId: '',
});
const redisForm = reactive({
key: '',
});
const roomRegionForm = reactive({
countryId: '',
userId: '',
});
const cancelRestoreForm = reactive({
countryId: '11',
userId: '',
});
const canExecuteUserSwap = computed(() => !userSwapBlocked.value);
const countryOptions = computed(() =>
countries.value.map((item) => ({
label: `${item.aliasName || '-'} / ${item.alphaTwo || '-'}`,
value: item.id as any,
})),
);
function prettyJson(value: any) {
try {
return JSON.stringify(value ?? {}, null, 2);
} catch {
return String(value ?? '');
}
}
function addApiAttribute() {
apiSignAttributes.value.push({ key: '', value: '' });
}
function clearApiAttributes() {
apiSignAttributes.value = [{ key: '', value: '' }];
}
function submitApiAttributes() {
const result = apiSignAttributes.value.reduce<Record<string, string>>(
(current, item) => {
if (item.key) {
current[item.key] = item.value || '';
}
return current;
},
{},
);
apiSignForm.body = JSON.stringify(result);
apiSignDialogOpen.value = false;
}
async function submitApiSign() {
if (!apiSignForm.platform || !apiSignForm.body || !apiSignForm.key) {
message.warning('签名平台、数据内容、签名KEY不能为空');
return;
}
apiSignLoading.value = true;
try {
apiSignResult.value = (await requestApiSing({ ...apiSignForm })) || '';
} finally {
apiSignLoading.value = false;
}
}
function addAliasAttribute() {
aliasForm.bets.push({ name: '', probability: '' });
}
function fillAliasAttributes() {
aliasForm.bets = ALIAS_FILLS.map((item) => ({ ...item }));
}
function clearAliasAttributes() {
aliasForm.bets = [{ name: '', probability: '' }];
}
async function submitAlias() {
const bets = aliasForm.bets.filter(
(item) => item.name && item.probability !== '',
);
if (bets.length === 0) {
message.warning('请至少设置一个奖品概率');
return;
}
aliasLoading.value = true;
try {
const result = (await alisMethod({
bets,
runSize: aliasForm.runSize,
})) || [];
renderEcharts({
tooltip: {},
xAxis: {
data: result.map((item) => `${item.name}/${Number(item.probability || 0) * 100}%`),
type: 'category',
},
yAxis: { type: 'value' },
series: [
{
data: result.map((item) => item.lottery),
type: 'bar',
},
],
});
aliasDialogOpen.value = false;
} finally {
aliasLoading.value = false;
}
}
async function checkSwapCondition() {
if (!userSwapForm.sourceUserId || !userSwapForm.targetUserId) {
message.warning('原始ID和目标ID不能为空');
return;
}
userSwapCheckLoading.value = true;
try {
const result = await checkUserSwapUser({ ...userSwapForm });
const list = result?.key || [];
userSwapResults.value = list
.map((item: string) => USER_SWAP_TIPS_MAP[item])
.filter(Boolean);
userSwapTips.value = result?.value || '';
userSwapBlocked.value = userSwapResults.value.some(
(item) => item.type === 'error',
);
} finally {
userSwapCheckLoading.value = false;
}
}
async function executeSwap() {
if (!canExecuteUserSwap.value) {
return;
}
userSwapExecuteLoading.value = true;
try {
await executeUserSwapUser({ ...userSwapForm });
message.success('执行成功');
userSwapBlocked.value = true;
userSwapResults.value = [];
userSwapTips.value = '';
} finally {
userSwapExecuteLoading.value = false;
}
}
async function submitTokenForm() {
tokenLoading.value = true;
try {
tokenResult.value =
tokenForm.mode === 'TOKEN_PARSE'
? (await getAccessTokenByUserId(tokenForm.token))
: (await getUserToken(tokenForm.userId));
} finally {
tokenLoading.value = false;
}
}
async function submitRedis() {
if (!redisForm.key) {
message.warning('请输入Key信息');
return;
}
redisLoading.value = true;
try {
await clearRedisKey(redisForm.key);
message.success('操作成功');
} finally {
redisLoading.value = false;
}
}
async function submitRoomRegion() {
if (!roomRegionForm.userId || !roomRegionForm.countryId) {
message.warning('用户ID和国家不能为空');
return;
}
roomRegionLoading.value = true;
try {
await updateUserRoomRegion({ ...roomRegionForm });
message.success('操作成功');
} finally {
roomRegionLoading.value = false;
}
}
async function submitCancelRestore() {
if (!cancelRestoreForm.userId) {
message.warning('请输入用户长ID');
return;
}
cancelRestoreLoading.value = true;
try {
await executeUserCancelAndRestore({ ...cancelRestoreForm });
message.success('操作成功');
} finally {
cancelRestoreLoading.value = false;
}
}
function filterCountry(input: string, option?: Record<string, any>) {
const label = String(option?.label || '').toLowerCase();
const value = String(option?.value || '').toLowerCase();
const keyword = input.toLowerCase();
return label.includes(keyword) || value.includes(keyword);
}
async function loadCountries() {
countries.value = (await getCountryAlls()) || [];
}
void loadCountries();
</script>
<template>
<Page title="小工具">
<div class="stack">
<Card title="用户账户调换">
<Form layout="vertical">
<div class="grid two">
<FormItem label="原始ID">
<Input v-model:value="userSwapForm.sourceUserId" placeholder="请输入原始用户长ID" />
</FormItem>
<FormItem label="目标ID">
<Input v-model:value="userSwapForm.targetUserId" placeholder="请输入目标用户长ID" />
</FormItem>
</div>
<FormItem>
<Button :loading="userSwapCheckLoading" type="primary" @click="checkSwapCondition">
验证转换条件
</Button>
</FormItem>
</Form>
<div v-if="userSwapResults.length > 0" class="tips-box">
<div
v-for="(item, index) in userSwapResults"
:key="index"
:class="['tips-item', item.type]"
>
{{ item.desc }} {{ userSwapTips }}
</div>
<Button
v-if="canExecuteUserSwap"
:loading="userSwapExecuteLoading"
type="primary"
@click="executeSwap"
>
提交执行
</Button>
</div>
</Card>
<Card title="API参数签名">
<Form layout="vertical">
<div class="grid three">
<FormItem label="签名平台">
<Select
option-label-prop="label"
v-model:value="apiSignForm.platform"
:options="ORIGIN_PLATFORM_OPTIONS"
/>
</FormItem>
<FormItem label="签名KEY">
<Input v-model:value="apiSignForm.key" />
</FormItem>
<FormItem label="操作">
<div class="inline-actions">
<Button type="default" @click="apiSignDialogOpen = true">设置属性</Button>
<Button :loading="apiSignLoading" type="primary" @click="submitApiSign">
提交
</Button>
</div>
</FormItem>
</div>
<FormItem label="数据内容">
<TextArea v-model:value="apiSignForm.body" :rows="4" />
</FormItem>
</Form>
<pre v-if="apiSignResult" class="result-pre">{{ apiSignResult }}</pre>
</Card>
<Card title="Alias Method分析">
<div class="inline-actions">
<Button type="default" @click="aliasDialogOpen = true">设置属性</Button>
<Button
:disabled="aliasForm.bets.length === 0"
:loading="aliasLoading"
type="primary"
@click="submitAlias"
>
重新运行
</Button>
</div>
<EchartsUI ref="aliasChartRef" style="height: 320px; margin-top: 16px" />
</Card>
<Card title="用户令牌解析">
<Form layout="vertical">
<FormItem label="模式">
<RadioGroup v-model:value="tokenForm.mode">
<Radio value="TOKEN_PARSE">令牌解析</Radio>
<Radio value="GET_TOKEN">获取令牌</Radio>
</RadioGroup>
</FormItem>
<FormItem v-if="tokenForm.mode === 'TOKEN_PARSE'" label="令牌">
<Input v-model:value="tokenForm.token" placeholder="请输入用户令牌" />
</FormItem>
<FormItem v-else label="用户ID">
<Input v-model:value="tokenForm.userId" placeholder="请输入用户ID" />
</FormItem>
<FormItem>
<Button :loading="tokenLoading" type="primary" @click="submitTokenForm">
确定
</Button>
</FormItem>
</Form>
<pre class="result-pre">{{ prettyJson(tokenResult) }}</pre>
</Card>
<Card title="Redis操作">
<Form layout="vertical">
<FormItem label="Key">
<Input v-model:value="redisForm.key" placeholder="请输入Key信息" />
</FormItem>
<FormItem>
<Button :loading="redisLoading" type="primary" @click="submitRedis">
确定
</Button>
</FormItem>
</Form>
</Card>
<Card title="用户房间区域变动">
<Form layout="vertical">
<div class="grid two">
<FormItem label="用户长ID">
<Input v-model:value="roomRegionForm.userId" placeholder="请输入原始用户长ID" />
</FormItem>
<FormItem label="国家">
<Select option-label-prop="label"
v-model:value="roomRegionForm.countryId"
:options="countryOptions"
show-search
:filter-option="filterCountry"
placeholder="请选择国家"
/>
</FormItem>
</div>
<FormItem>
<Button :loading="roomRegionLoading" type="primary" @click="submitRoomRegion">
确定
</Button>
</FormItem>
</Form>
</Card>
<Card title="注销用户恢复">
<Form layout="vertical">
<FormItem label="用户长ID">
<Input v-model:value="cancelRestoreForm.userId" placeholder="请输入原始用户长ID" />
</FormItem>
<FormItem>
<Button :loading="cancelRestoreLoading" type="primary" @click="submitCancelRestore">
确定
</Button>
</FormItem>
</Form>
</Card>
</div>
<Modal
:confirm-loading="apiSignLoading"
:open="apiSignDialogOpen"
title="API内容属性"
@cancel="apiSignDialogOpen = false"
@ok="submitApiAttributes"
>
<div class="stack-sm">
<div
v-for="(item, index) in apiSignAttributes"
:key="index"
class="grid two"
>
<Input v-model:value="item.key" placeholder="键" />
<Input v-model:value="item.value" placeholder="值" />
</div>
<div class="inline-actions">
<Button @click="clearApiAttributes">清空属性</Button>
<Button @click="addApiAttribute">新增属性</Button>
</div>
</div>
</Modal>
<Modal
:confirm-loading="aliasLoading"
:open="aliasDialogOpen"
title="设置概率"
width="760px"
@cancel="aliasDialogOpen = false"
@ok="submitAlias"
>
<div class="stack-sm">
<div class="tips-plain">
<div>平均概率: 1.0 / 奖品数量 = {{ aliasForm.bets.length > 0 ? 1 / aliasForm.bets.length : 0 }}</div>
<div>概率填充: 概率大于等于平均概率进入 large否则进入 small</div>
</div>
<Form layout="vertical">
<FormItem label="运行次数">
<Input v-model:value="aliasForm.runSize" />
</FormItem>
</Form>
<div
v-for="(item, index) in aliasForm.bets"
:key="index"
class="grid two"
>
<Input v-model:value="item.name" placeholder="商品" />
<Input v-model:value="item.probability" placeholder="概率" />
</div>
<div class="inline-actions">
<Button @click="clearAliasAttributes">清空属性</Button>
<Button @click="addAliasAttribute">新增属性</Button>
<Button @click="fillAliasAttributes">快速填充</Button>
</div>
</div>
</Modal>
</Page>
</template>
<style scoped>
.stack {
display: grid;
gap: 16px;
}
.stack-sm {
display: grid;
gap: 12px;
}
.grid {
display: grid;
gap: 12px;
}
.grid.two {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid.three {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.inline-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tips-box {
background: rgb(248 250 252);
border-radius: 12px;
margin-top: 12px;
padding: 12px;
}
.tips-item {
margin-bottom: 8px;
}
.tips-item.error {
color: rgb(185 28 28);
}
.tips-item.warn {
color: rgb(180 83 9);
}
.tips-plain {
color: rgb(71 85 105);
line-height: 1.8;
}
.result-pre {
background: rgb(248 250 252);
border-radius: 12px;
margin: 0;
overflow: auto;
padding: 16px;
white-space: pre-wrap;
word-break: break-all;
}
@media (max-width: 960px) {
.grid.two,
.grid.three {
grid-template-columns: minmax(0, 1fr);
}
}
</style>