feat: add register reward daily records

This commit is contained in:
hy 2026-04-20 13:45:50 +08:00
parent ca35226a1c
commit 474caa56e0
3 changed files with 263 additions and 4 deletions

View File

@ -45,6 +45,15 @@ export async function resetResidentRegisterRewardConfig(sysOrigin: string) {
});
}
export async function pageResidentRegisterRewardDailyRecords(
params: Record<string, any>,
) {
return requestClient.get<Record<string, any>>(
'/go/resident-activity/register-reward/daily-record/page',
{ params },
);
}
export async function pageSpecifiedGiftWeeklyRankConfigs(params: Record<string, any>) {
return requestClient.get<LegacyPageResult<Record<string, any>>>(
`${SPECIFIED_GIFT_WEEKLY_RANK_BASE}/config/page`,

View File

@ -6,21 +6,26 @@ import { useAccessStore } from '@vben/stores';
import {
getResidentRegisterRewardConfig,
pageResidentRegisterRewardDailyRecords,
resetResidentRegisterRewardConfig,
saveResidentRegisterRewardConfig,
} from '#/api/legacy/resident-activity';
import ActivityResourceGroupSelectDrawer from '#/views/props/components/activity-resource-group-select-drawer.vue';
import UserProfileLink from '#/views/operate/components/user-profile-link.vue';
import { getAllowedSysOrigins } from '#/views/system/shared';
import {
Button,
Card,
DatePicker,
InputNumber,
Modal,
Pagination,
Select,
Space,
Spin,
Switch,
Table,
Tag,
message,
} from 'antdv-next';
@ -35,10 +40,14 @@ const sysOriginOptions = computed(() => {
const loading = ref(false);
const saving = ref(false);
const recordLoading = ref(false);
const recordTotal = ref(0);
const groupPickerOpen = ref(false);
const recordList = ref<Array<Record<string, any>>>([]);
const form = reactive<Record<string, any>>({
configured: false,
dailyLimit: 0,
enabled: false,
goldAmount: 0,
rewardGroupId: null,
@ -46,6 +55,22 @@ const form = reactive<Record<string, any>>({
sysOrigin: '',
});
const recordQuery = reactive<Record<string, any>>({
claimDate: '',
cursor: 1,
limit: 20,
});
const recordSummary = reactive<Record<string, any>>({
dailyLimit: 0,
date: '',
issuedCount: 0,
limitEnabled: false,
occupiedCount: 0,
remainingCount: 0,
successCount: 0,
});
const statusMeta = computed(() => {
if (!form.configured) {
return {
@ -57,7 +82,10 @@ const statusMeta = computed(() => {
if (form.enabled) {
return {
color: 'success',
description: '当前已启用,注册后会按下面配置发金币和/或奖励组。',
description:
form.dailyLimit > 0
? `当前已启用,每日最多发放 ${form.dailyLimit} 份注册奖励。`
: '当前已启用,注册后会按下面配置发金币和/或奖励组。',
text: '已启用',
};
}
@ -68,9 +96,20 @@ const statusMeta = computed(() => {
};
});
const recordColumns = [
{ dataIndex: 'user', key: 'user', title: '领取用户', width: 280 },
{ dataIndex: 'claimResult', key: 'claimResult', title: '领取结果', width: 220 },
{ dataIndex: 'status', key: 'status', title: '处理状态', width: 120 },
{ dataIndex: 'rewardPayload', key: 'rewardPayload', title: '奖励内容', width: 220 },
{ dataIndex: 'claimDate', key: 'claimDate', title: '领取日期', width: 120 },
{ dataIndex: 'createTime', key: 'createTime', title: '触发时间', width: 180 },
{ dataIndex: 'errorMessage', key: 'errorMessage', title: '备注 / 失败原因', width: 260 },
];
function normalizeConfig(result: Record<string, any> | null | undefined) {
const value = result ?? {};
form.configured = Boolean(value.configured);
form.dailyLimit = Number(value.dailyLimit || 0);
form.enabled = Boolean(value.enabled);
form.goldAmount = Number(value.goldAmount || 0);
form.rewardGroupId = value.rewardGroupId ?? null;
@ -78,7 +117,46 @@ function normalizeConfig(result: Record<string, any> | null | undefined) {
form.sysOrigin = String(value.sysOrigin || form.sysOrigin || sysOriginOptions.value[0]?.value || '');
}
async function loadData() {
function syncRecordSummary(result: Record<string, any> | null | undefined) {
const value = result ?? {};
recordSummary.date = String(value.date || recordSummary.date || '');
recordSummary.dailyLimit = Number(value.dailyLimit || 0);
recordSummary.limitEnabled = Boolean(value.limitEnabled);
recordSummary.occupiedCount = Number(value.occupiedCount || 0);
recordSummary.issuedCount = Number(value.issuedCount || 0);
recordSummary.successCount = Number(value.successCount || 0);
recordSummary.remainingCount = Number(value.remainingCount || 0);
}
function claimResultColor(result: string) {
switch (String(result || '').toUpperCase()) {
case 'REWARDED':
return 'success';
case 'QUOTA_EXHAUSTED':
return 'warning';
case 'FAILED':
return 'error';
case 'SKIPPED':
return 'default';
default:
return 'processing';
}
}
function processStatusColor(status: string) {
switch (String(status || '').toUpperCase()) {
case 'SUCCESS':
return 'success';
case 'FAILED':
return 'error';
case 'SKIPPED':
return 'default';
default:
return 'processing';
}
}
async function loadConfig() {
if (!form.sysOrigin) {
return;
}
@ -91,6 +169,36 @@ async function loadData() {
}
}
async function loadDailyRecords(reset = false) {
if (!form.sysOrigin) {
recordList.value = [];
recordTotal.value = 0;
return;
}
if (reset) {
recordQuery.cursor = 1;
}
recordLoading.value = true;
try {
const result = await pageResidentRegisterRewardDailyRecords({
...recordQuery,
sysOrigin: form.sysOrigin,
});
recordList.value = Array.isArray(result.records) ? result.records : [];
recordTotal.value = Number(result.total || 0);
syncRecordSummary(result);
if (!recordQuery.claimDate && result.date) {
recordQuery.claimDate = String(result.date);
}
} finally {
recordLoading.value = false;
}
}
async function loadData() {
await Promise.all([loadConfig(), loadDailyRecords(true)]);
}
watch(
sysOriginOptions,
(options) => {
@ -130,6 +238,7 @@ async function submitForm() {
saving.value = true;
try {
await saveResidentRegisterRewardConfig({
dailyLimit: Number(form.dailyLimit || 0),
enabled: Boolean(form.enabled),
goldAmount: Number(form.goldAmount || 0),
rewardGroupId: form.rewardGroupId ?? null,
@ -167,6 +276,16 @@ function handleReset() {
content: '清空后会删除当前系统的后台配置,普通注册成功后不再发放注册奖励。',
});
}
function handleRecordSearch() {
void loadDailyRecords(true);
}
function handleRecordPageChange(page: number, pageSize: number) {
recordQuery.cursor = page;
recordQuery.limit = pageSize;
void loadDailyRecords();
}
</script>
<template>
@ -206,6 +325,19 @@ function handleReset() {
/>
</div>
<div class="section">
<div class="field-label">每日限制份数</div>
<InputNumber
v-model:value="form.dailyLimit"
:disabled="!form.enabled"
:min="0"
:precision="0"
placeholder="0 表示不限"
style="width: 240px"
/>
<span class="field-desc">0 表示不限超过当天限额后新用户会视为领取成功但不再真实发奖</span>
</div>
<div class="section">
<div class="field-label">赠送奖励组</div>
<Space wrap>
@ -233,7 +365,7 @@ function handleReset() {
<div class="tips">
<div>未配置注册成功后不发奖励</div>
<div>已启用可以同时送金币和奖励组两者至少配置一个</div>
<div>已关闭注册成功后不发奖励</div>
<div>每日限制份数达到上限后后续用户会直接记为领取成功不再真实发奖</div>
</div>
<Space class="actions" wrap>
@ -246,6 +378,95 @@ function handleReset() {
</Space>
</Card>
<Card :bordered="false" class="record-card" title="每日领取记录">
<div class="toolbar">
<Space wrap>
<DatePicker
v-model:value="recordQuery.claimDate"
placeholder="领取日期"
style="width: 160px"
value-format="YYYY-MM-DD"
/>
<Button :loading="recordLoading" type="primary" @click="handleRecordSearch">
查询
</Button>
</Space>
</div>
<div class="summary">
<Tag color="processing">
统计日期{{ recordSummary.date || '-' }}
</Tag>
<Tag color="purple">
每日限额{{ recordSummary.limitEnabled ? `${recordSummary.dailyLimit}` : '不限' }}
</Tag>
<Tag color="orange">
已占用名额{{ recordSummary.occupiedCount }}
</Tag>
<Tag color="success">
已发放成功{{ recordSummary.issuedCount }}
</Tag>
<Tag color="cyan">
领取成功人数{{ recordSummary.successCount }}
</Tag>
<Tag color="default">
剩余名额{{ recordSummary.limitEnabled ? `${recordSummary.remainingCount}` : '不限' }}
</Tag>
</div>
<Table
:columns="recordColumns"
:data-source="recordList"
:loading="recordLoading"
:pagination="false"
row-key="id"
:scroll="{ x: 1280 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'user'">
<UserProfileLink
:profile="{
account: record.account,
countryCode: record.countryCode,
countryName: record.countryName,
id: record.userId,
userAvatar: record.userAvatar,
userNickname: record.userNickname,
}"
/>
</template>
<template v-else-if="column.key === 'claimResult'">
<Tag :color="claimResultColor(record.claimResult)">
{{ record.claimResultText || '-' }}
</Tag>
</template>
<template v-else-if="column.key === 'status'">
<Tag :color="processStatusColor(record.status)">
{{ record.status || '-' }}
</Tag>
</template>
<template v-else-if="column.key === 'rewardPayload'">
<div>金币{{ record.goldAmount || 0 }}</div>
<div>奖励组{{ record.rewardGroupId || '-' }}</div>
</template>
<template v-else-if="column.key === 'errorMessage'">
{{ record.errorMessage || '-' }}
</template>
</template>
</Table>
<div v-if="recordTotal > 0" class="pager">
<Pagination
:current="recordQuery.cursor"
:page-size="recordQuery.limit"
:total="recordTotal"
show-size-changer
@change="handleRecordPageChange"
@showSizeChange="handleRecordPageChange"
/>
</div>
</Card>
<ActivityResourceGroupSelectDrawer
:open="groupPickerOpen"
:sys-origin="form.sysOrigin"
@ -258,7 +479,7 @@ function handleReset() {
<style scoped>
.toolbar {
margin-bottom: 24px;
margin-bottom: 16px;
}
.status-desc {
@ -278,6 +499,10 @@ function handleReset() {
font-weight: 600;
}
.field-desc {
color: rgb(100 116 139);
}
.reward-group {
color: rgb(15 23 42);
}
@ -298,4 +523,21 @@ function handleReset() {
.actions {
margin-top: 24px;
}
.record-card {
margin-top: 16px;
}
.summary {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 16px;
}
.pager {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
</style>

View File

@ -5,6 +5,7 @@ const DEV_API_INTERNAL_PREFIX = '/__dev_api_internal__';
const DEV_API_PROD_PREFIX = '/__dev_api_prod__';
const DEV_INTERNAL_CONSOLE_TARGET = 'http://127.0.0.1:2700';
const DEV_INTERNAL_GOLANG_TARGET = 'http://127.0.0.1:2900';
const DEV_PROD_GOLANG_TARGET = 'https://jvapi.haiyihy.com';
const DEV_PROD_TARGET = 'https://yumi-admin.haiyihy.com';
function createPrefixRegExp(prefix: string) {
@ -31,6 +32,13 @@ export default defineConfig(async () => {
target: DEV_INTERNAL_CONSOLE_TARGET,
ws: true,
},
[`${DEV_API_PROD_PREFIX}/go`]: {
changeOrigin: true,
rewrite: (path) =>
path.replace(createPrefixRegExp(DEV_API_PROD_PREFIX), ''),
target: DEV_PROD_GOLANG_TARGET,
ws: true,
},
[DEV_API_PROD_PREFIX]: {
changeOrigin: true,
rewrite: (path) =>