fix and add
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled

This commit is contained in:
2026-01-03 17:53:43 +08:00
parent c80d8383c3
commit 27d63095a9
3 changed files with 343 additions and 109 deletions

View File

@@ -78,6 +78,7 @@ export namespace AgentApi {
page: number;
pageSize: number;
agent_id?: number;
order_id?: number;
product_name?: string;
status?: number;
}
@@ -333,6 +334,13 @@ export interface GetAgentLinkProductStatisticsParams {}
price_ratio?: null | number; // 提价区间收取比例
price_increase_amount?: null | number; // 在原本成本上加价的金额
}
// 代理钱包信息
export interface AgentWalletInfo {
balance: number; // 可用余额
frozen_balance: number; // 冻结余额
total_earnings: number; // 总收益
}
}
/**
@@ -571,6 +579,15 @@ async function getAgentLinkProductStatistics() {
);
}
/**
* 获取代理钱包信息
*/
async function getAgentWallet(agentId: number) {
return requestClient.get<AgentApi.AgentWalletInfo>(
`/agent/wallet/${agentId}`,
);
}
export {
batchUnfreezeAgentCommission,
getAgentCommissionDeductionList,
@@ -584,6 +601,7 @@ export {
getAgentProductionConfigList,
getAgentRewardList,
getAgentStatistics,
getAgentWallet,
getAgentWithdrawalList,
getMembershipRechargeOrderList,
getWithdrawalStatistics,

View File

@@ -37,7 +37,7 @@ export function useCommissionColumns(
const statusMap: Record<number, string> = {
0: '已结算',
1: '冻结中',
2: '已取消',
2: '已退款',
};
return statusMap[cellValue] || '未知';
},
@@ -83,21 +83,7 @@ export function useCommissionColumns(
row?.status !== 1 ? '!text-gray-400 !cursor-not-allowed' : '',
tooltip: (row: AgentApi.AgentCommissionListItem) => {
if (row?.status === 0) return '已结算的佣金无需解冻';
if (row?.status === 2) return '已取消的佣金无法操作';
return '';
},
},
{
code: 'cancel',
text: '取消',
type: 'primary',
disabled: (row: AgentApi.AgentCommissionListItem) =>
row?.status !== 1,
class: (row: AgentApi.AgentCommissionListItem) =>
row?.status !== 1 ? '!text-gray-400 !cursor-not-allowed' : '',
tooltip: (row: AgentApi.AgentCommissionListItem) => {
if (row?.status === 0) return '只能取消冻结中的佣金';
if (row?.status === 2) return '已取消的佣金无法再次取消';
if (row?.status === 2) return '已退款的佣金无法操作';
return '';
},
},
@@ -114,10 +100,22 @@ export function useCommissionColumns(
// 佣金记录搜索表单配置
export function useCommissionFormSchema(): VbenFormSchema[] {
return [
{
component: 'InputNumber',
fieldName: 'order_id',
label: '订单ID',
componentProps: {
placeholder: '请输入订单ID',
style: { width: '100%' },
},
},
{
component: 'Input',
fieldName: 'product_name',
label: '产品名称',
componentProps: {
placeholder: '请输入产品名称(支持模糊搜索)',
},
},
{
component: 'Select',
@@ -125,10 +123,11 @@ export function useCommissionFormSchema(): VbenFormSchema[] {
label: '状态',
componentProps: {
allowClear: true,
placeholder: '请选择状态',
options: [
{ label: '已结算', value: 0 },
{ label: '冻结中', value: 1 },
{ label: '已取消', value: 2 },
{ label: '已退款', value: 2 },
],
},
},

View File

@@ -1,15 +1,18 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, h, onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Button, message, Modal } from 'ant-design-vue';
import { Button, message, Modal, Select } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import {
batchUnfreezeAgentCommission,
getAgentCommissionList,
getAgentList,
getAgentWallet,
updateAgentCommissionStatus,
} from '#/api/agent';
import type { AgentApi } from '#/api';
import { useCommissionColumns, useCommissionFormSchema } from './data';
@@ -17,18 +20,77 @@ interface Props {
agentId?: number;
}
interface QueryParams {
currentPage: number;
pageSize: number;
[key: string]: any;
}
const props = defineProps<Props>();
// 用于一键解冻筛选的代理商ID
const unfreezeAgentId = ref<number | undefined>();
// 代理商列表(完整列表)
const allAgentList = ref<AgentApi.AgentListItem[]>([]);
// 显示在下拉框中的代理商列表(可能是过滤后的)
const agentList = ref<AgentApi.AgentListItem[]>([]);
const queryParams = computed(() => ({
...(props.agentId ? { agent_id: props.agentId } : {}),
...(unfreezeAgentId.value ? { agent_id: unfreezeAgentId.value } : {}),
}));
// 加载代理商列表
async function loadAgentList() {
try {
const result = await getAgentList({ page: 1, pageSize: 10000 });
allAgentList.value = result.items || [];
agentList.value = result.items || [];
} catch (error) {
console.error('加载代理商列表失败:', error);
message.error('加载代理商列表失败,请刷新页面重试');
}
}
// 搜索时动态过滤代理商支持ID和手机号
function onAgentSearch(value: string) {
if (!value || value.trim() === '') {
// 如果输入为空,显示所有代理商
agentList.value = allAgentList.value;
return;
}
const searchValue = value.trim();
// 从完整列表中过滤匹配的代理商
const filtered = allAgentList.value.filter(agent => {
// 匹配代理ID
if (agent.id.toString().includes(searchValue)) {
return true;
}
// 匹配手机号
if (agent.mobile && agent.mobile.includes(searchValue)) {
return true;
}
// 匹配姓名(如果存在)
if (agent.real_name && agent.real_name.includes(searchValue)) {
return true;
}
return false;
});
agentList.value = filtered;
}
// 获取未找到时的提示文案
function getNotFoundContent() {
if (unfreezeAgentId.value) {
return '点击确认使用此ID';
}
return '暂无代理商';
}
// 页面加载时获取代理商列表
onMounted(() => {
loadAgentList();
});
// 操作处理函数
function onActionClick({ code, row }: { code: string; row: any }) {
switch (code) {
@@ -38,70 +100,88 @@ function onActionClick({ code, row }: { code: string; row: any }) {
case 'unfreeze':
onUnfreeze(row);
break;
case 'cancel':
onCancel(row);
break;
}
}
// 冻结佣金
async function onFreeze(row: any) {
Modal.confirm({
title: '确认冻结',
content: `确定要冻结佣金金额 ¥${row.amount.toFixed(2)} 吗?冻结后将暂时无法提现。`,
okText: '确认冻结',
cancelText: '取消',
onOk: async () => {
try {
await updateAgentCommissionStatus(row.id, 1);
message.success('佣金已冻结');
onRefresh();
} catch (error: any) {
const errorMsg = error?.response?.data?.msg || error?.message || '操作失败,请重试';
message.error(errorMsg);
}
try {
// 先获取代理商钱包信息,检查余额是否充足
const hideChecking = message.loading({
content: '正在检查余额...',
duration: 0,
key: 'check_balance',
});
const wallet = await getAgentWallet(row.agent_id);
hideChecking();
// 检查余额是否充足
if (wallet.balance < row.amount) {
Modal.warning({
title: '余额不足',
content: `该代理商当前可用余额为 ¥${wallet.balance.toFixed(2)},不足以冻结佣金 ¥${row.amount.toFixed(2)}\n缺少金额¥${(row.amount - wallet.balance).toFixed(2)}`,
okText: '我知道了',
});
return;
}
});
// 余额充足,继续冻结操作
Modal.confirm({
title: '确认冻结',
content: `确定要冻结佣金金额 ¥${row.amount.toFixed(2)} 吗?\n当前可用余额¥${wallet.balance.toFixed(2)},冻结后可用余额:¥${(wallet.balance - row.amount).toFixed(2)}`,
okText: '确认冻结',
cancelText: '取消',
onOk: async () => {
try {
await updateAgentCommissionStatus(row.id, 1);
message.success('佣金已冻结');
onRefresh();
} catch (error: any) {
const errorMsg = error?.response?.data?.msg || error?.message || '操作失败,请重试';
message.error(errorMsg);
}
},
});
} catch (error: any) {
const errorMsg = error?.response?.data?.msg || error?.message || '获取钱包信息失败,请重试';
message.error(errorMsg);
}
}
// 解冻佣金到用户余额
async function onUnfreeze(row: any) {
Modal.confirm({
title: '确认解冻',
content: `确定要解冻佣金金额 ¥${row.amount.toFixed(2)} 吗?解冻后将转入用户钱包余额。`,
okText: '确认解冻',
cancelText: '取消',
onOk: async () => {
try {
await updateAgentCommissionStatus(row.id, 0);
message.success('佣金已解冻并转入用户钱包余额');
onRefresh();
} catch (error: any) {
const errorMsg = error?.response?.data?.msg || error?.message || '操作失败,请重试';
message.error(errorMsg);
}
}
});
}
try {
// 获取代理商钱包信息用于显示
const hideChecking = message.loading({
content: '正在获取钱包信息...',
duration: 0,
key: 'get_wallet',
});
// 取消佣金
async function onCancel(row: any) {
Modal.confirm({
title: '确认取消',
content: `确定要取消佣金金额 ¥${row.amount.toFixed(2)} 吗?取消后将无法恢复。`,
okText: '确认取消',
cancelText: '返回',
onOk: async () => {
try {
await updateAgentCommissionStatus(row.id, 2);
message.success('佣金已取消');
onRefresh();
} catch (error: any) {
const errorMsg = error?.response?.data?.msg || error?.message || '操作失败,请重试';
message.error(errorMsg);
}
}
});
const wallet = await getAgentWallet(row.agent_id);
hideChecking();
Modal.confirm({
title: '确认解冻',
content: `确定要解冻佣金金额 ¥${row.amount.toFixed(2)} 吗?\n解冻后将转入用户钱包余额。\n当前可用余额¥${wallet.balance.toFixed(2)},解冻后可用余额:¥${(wallet.balance + row.amount).toFixed(2)}`,
okText: '确认解冻',
cancelText: '取消',
onOk: async () => {
try {
await updateAgentCommissionStatus(row.id, 0);
message.success('佣金已解冻并转入用户钱包余额');
onRefresh();
} catch (error: any) {
const errorMsg = error?.response?.data?.msg || error?.message || '操作失败,请重试';
message.error(errorMsg);
}
},
});
} catch (error: any) {
const errorMsg = error?.response?.data?.msg || error?.message || '获取钱包信息失败,请重试';
message.error(errorMsg);
}
}
// 刷新列表
@@ -109,29 +189,141 @@ function onRefresh() {
gridApi.query();
}
// 选中代理商后刷新表格
function onAgentSelect(value: any) {
console.log('选中代理商:', value);
// 如果是清空选择value 为 undefined
// 如果有值,转换为数字
unfreezeAgentId.value = value ? parseInt(String(value), 10) : undefined;
// 延迟一点让 computed 更新后再刷新
setTimeout(() => {
onRefresh();
}, 100);
}
// 批量解冻佣金
async function onBatchUnfreeze() {
const content = props.agentId
? '确定要一键解冻当前代理商所有冻结中的佣金吗?解冻后将全部转入用户钱包余额。'
: '确定要一键解冻所有冻结中的佣金吗?解冻后将全部转入用户钱包余额。';
const targetAgentId = unfreezeAgentId.value || props.agentId;
Modal.confirm({
title: '批量解冻确认',
content,
okText: '确认解冻',
cancelText: '取消',
onOk: async () => {
try {
const result = await batchUnfreezeAgentCommission(props.agentId);
message.success(
`批量解冻成功!共解冻 ${result.count} 条记录,总金额 ¥${result.amount.toFixed(2)}`,
);
onRefresh();
} catch (error) {
message.error('批量解冻失败,请重试');
}
},
const hideChecking = message.loading({
content: '正在检查数据,请稍候...',
duration: 0,
key: 'check_frozen_data',
});
try {
// 1. 查询所有冻结中的佣金记录
const frozenCommissions = await getAgentCommissionList({
page: 1,
pageSize: 10000,
status: 1,
...(targetAgentId ? { agent_id: targetAgentId } : {}),
});
hideChecking();
// 如果没有冻结的佣金,直接返回
if (!frozenCommissions.items || frozenCommissions.items.length === 0) {
message.info({
content: '没有需要解冻的冻结佣金',
key: 'check_frozen_data',
});
return;
}
// 2. 统计每个代理商的佣金冻结金额
const commissionFrozenMap = new Map<number, number>();
frozenCommissions.items.forEach((item: any) => {
const currentAmount = commissionFrozenMap.get(item.agent_id) || 0;
commissionFrozenMap.set(item.agent_id, currentAmount + item.amount);
});
// 3. 检查每个代理商的钱包冻结余额是否足够
let insufficientAgents: string[] = [];
let totalFrozenAmount = 0;
for (const [agentId, commissionAmount] of commissionFrozenMap) {
totalFrozenAmount += commissionAmount;
// 获取该代理商的钱包信息
const wallet = await getAgentWallet(agentId);
if (wallet.frozen_balance < commissionAmount) {
// 找到该代理商的信息
const agent = allAgentList.value.find(a => a.id === agentId);
const agentInfo = agent ? `${agent.real_name || agent.mobile} (ID: ${agentId})` : `ID: ${agentId}`;
insufficientAgents.push(
`${agentInfo}:钱包冻结余额 ¥${wallet.frozen_balance.toFixed(2)},需要解冻 ¥${commissionAmount.toFixed(2)},缺少 ¥${(commissionAmount - wallet.frozen_balance).toFixed(2)}`
);
}
}
// 如果有余额不足的代理商,显示错误信息
if (insufficientAgents.length > 0) {
Modal.error({
title: '冻结余额不足,无法解冻',
width: 600,
content: h('div', [
h('p', '以下代理商的钱包冻结余额不足以解冻其冻结的佣金:'),
h('ul', { style: { 'max-height': '300px', 'overflow-y': 'auto', 'padding-left': '20px' } },
insufficientAgents.map(msg => h('li', { style: { marginBottom: '8px' } }, msg))
),
h('p', { style: { marginTop: '16px', color: '#999' } }, '请先核实钱包冻结余额数据,或联系技术支持。'),
]),
okText: '我知道了',
});
return;
}
// 4. 余额检查通过,确认解冻
const content = targetAgentId
? `确定要一键解冻代理商 ID: ${targetAgentId} 所有冻结中的佣金吗?\n\n共 ${frozenCommissions.items.length} 条记录,总金额 ¥${totalFrozenAmount.toFixed(2)}\n解冻后将全部转入用户钱包余额。`
: `确定要一键解冻所有冻结中的佣金吗?\n\n共 ${frozenCommissions.items.length} 条记录,总金额 ¥${totalFrozenAmount.toFixed(2)}\n解冻后将全部转入用户钱包余额。`;
Modal.confirm({
title: '批量解冻确认',
content,
okText: '确认解冻',
cancelText: '取消',
onOk: async () => {
const hideLoading = message.loading({
content: '正在批量解冻佣金,请稍候...',
duration: 0,
key: 'batch_unfreeze',
});
try {
const result = await batchUnfreezeAgentCommission(targetAgentId);
message.success({
content: `批量解冻成功!共解冻 ${result.count} 条记录,总金额 ¥${result.amount.toFixed(2)}`,
key: 'batch_unfreeze',
});
onRefresh();
} catch (error: any) {
hideLoading();
const errorMsg = error?.response?.data?.msg || error?.message || '批量解冻失败,请重试';
// 如果是版本冲突错误,给出更友好的提示
if (errorMsg.includes('update db no rows change') || errorMsg.includes('版本冲突') || errorMsg.includes('状态已被其他操作修改')) {
message.error({
content: '批量解冻失败:部分佣金或钱包数据已被其他操作修改,请稍后重试。如果问题持续,请联系管理员。',
key: 'batch_unfreeze',
});
} else {
message.error({
content: `批量解冻失败:${errorMsg}`,
key: 'batch_unfreeze',
});
}
}
},
});
} catch (error: any) {
hideChecking();
const errorMsg = error?.response?.data?.msg || error?.message || '数据检查失败,请重试';
message.error({
content: `检查失败:${errorMsg}`,
key: 'check_frozen_data',
});
}
}
const [Grid, gridApi] = useVbenVxeGrid({
@@ -155,16 +347,15 @@ const [Grid, gridApi] = useVbenVxeGrid({
},
proxyConfig: {
ajax: {
query: async ({
page,
form,
}: {
form: Record<string, any>;
page: QueryParams;
}) => {
query: async ({ page, form, sort }: any, formValues: Record<string, any>) => {
console.log('=== 佣金列表查询参数 ===');
console.log('第一个参数 (params):', { page, form, sort });
console.log('第二个参数 (formValues):', formValues);
console.log(' queryParams.value:', queryParams.value);
return await getAgentCommissionList({
...queryParams.value,
...form,
...formValues,
page: page.currentPage,
pageSize: page.pageSize,
});
@@ -174,6 +365,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
result: 'items',
total: 'total',
},
autoLoad: true,
},
},
});
@@ -183,10 +375,35 @@ const [Grid, gridApi] = useVbenVxeGrid({
<Page :auto-content-height="!agentId">
<Grid :table-title="agentId ? '佣金记录列表' : '所有佣金记录'">
<template #toolbar-tools>
<Button type="primary" @click="onBatchUnfreeze">
<span class="mr-1"></span>
一键解冻
</Button>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-600">选择代理商:</span>
<Select
v-model:value="unfreezeAgentId"
placeholder="全部代理商 / 输入代理ID或手机号"
:allow-clear="true"
:loading="agentList.length === 0"
style="width: 260px"
show-search
:filter-option="false"
:show-arrow="true"
:not-found-content="getNotFoundContent()"
@search="onAgentSearch"
@change="onAgentSelect"
>
<Select.Option
v-for="agent in agentList"
:key="agent.id"
:value="agent.id"
:label="`${agent.real_name || agent.mobile} (ID: ${agent.id})`"
>
{{ agent.real_name || agent.mobile }} (ID: {{ agent.id }})
</Select.Option>
</Select>
<Button type="primary" @click="onBatchUnfreeze">
<span class="mr-1"></span>
一键解冻
</Button>
</div>
</template>
</Grid>
</Page>