feat(admin): 用户列表新增「调余额」人工充值/扣减入口

- 新接口 POST /api/admin/users/:id/balance(requireAdmin):事务锁行、扣减不可为负、单次上限 100 万、写 finance_records 并推送站内通知

- 后台用户列表每行新增「调余额」按钮与弹窗(当前余额/方向/金额/备注)

- 新增 admin-balance-adjust 护栏;113 套件/737 用例全绿
这个提交包含在:
xcbyy
2026-10-21 17:41:54 +08:00
父节点 4e3283ff01
当前提交 14ce4b25f5
共有 4 个文件被更改,包括 238 次插入 和 1 次删除

查看文件

@@ -226,6 +226,11 @@
详情
</a-button>
<a-button type="outline" size="small" @click.stop="openBalanceModal(record)">
<template #icon><icon-plus /></template>
调余额
</a-button>
<a-popconfirm
:content="record.status === '正常' ? `确定要封禁用户 [${record.nickname}] 吗?封禁后将限制发单与接单。` : `确定要解除用户 [${record.nickname}] 的封禁状态吗?`"
ok-text="确认"
@@ -601,6 +606,53 @@
</a-tabs>
</div>
</a-drawer>
<!-- 人工调整钱包余额(运营/测试) -->
<a-modal
v-model:visible="balanceModalVisible"
title="调整用户钱包余额"
:width="480"
:ok-loading="submittingBalance"
ok-text="确认调整"
cancel-text="取消"
@ok="submitBalanceAdjust"
>
<a-form :model="balanceForm" layout="vertical">
<a-form-item label="用户">
<a-input :model-value="`${balanceTarget.nickname || ''}(UID: ${balanceTarget.id || ''})`" disabled />
</a-form-item>
<a-form-item label="当前余额">
<a-input :model-value="`¥${Number(balanceTarget.balance || 0).toFixed(2)}`" disabled />
</a-form-item>
<a-form-item label="调整方向">
<a-radio-group v-model="balanceForm.direction">
<a-radio value="IN">充值(增加余额)</a-radio>
<a-radio value="OUT">扣减(减少余额)</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="金额(元)" required>
<a-input-number
v-model="balanceForm.amount"
:min="0.01"
:max="100000"
:precision="2"
style="width: 100%"
placeholder="请输入调整金额"
/>
</a-form-item>
<a-form-item label="备注(写入用户财务流水)">
<a-input
v-model="balanceForm.reason"
placeholder="如:测试充值 / 活动赠送 / 人工补偿"
:max-length="80"
show-word-limit
/>
</a-form-item>
<a-alert type="warning">
该操作会真实修改用户余额并写入财务流水;扣减时不能超过当前余额,请谨慎操作。
</a-alert>
</a-form>
</a-modal>
</div>
</template>
@@ -622,6 +674,52 @@
const currentUser = ref<any>(null);
const userDetail = ref<any>(null);
// 人工调整余额
const balanceModalVisible = ref(false);
const balanceTarget = ref<any>({});
const balanceForm = ref({ direction: 'IN', amount: 100, reason: '' });
const submittingBalance = ref(false);
const openBalanceModal = (record: any) => {
balanceTarget.value = { ...(record || {}) };
balanceForm.value = { direction: 'IN', amount: 100, reason: '' };
balanceModalVisible.value = true;
};
const submitBalanceAdjust = async () => {
const amount = Number(balanceForm.value.amount);
if (!amount || amount <= 0) {
Message.warning('请输入有效的调整金额');
return;
}
if (!balanceTarget.value.id) {
Message.error('用户信息缺失,请刷新后重试');
return;
}
submittingBalance.value = true;
try {
const res: any = await axios.post(`/api/admin/users/${balanceTarget.value.id}/balance`, {
amount,
direction: balanceForm.value.direction,
reason: balanceForm.value.reason,
});
if (res && res.data && res.data.code === 20000) {
Message.success(res.data.message || '余额调整成功');
balanceModalVisible.value = false;
await fetchUsers();
if (detailDrawerVisible.value && currentUser.value) {
fetchUserDetail(currentUser.value.id);
}
} else {
Message.error((res && res.data && res.data.message) || '余额调整失败');
}
} catch (err: any) {
Message.error((err && err.message) || '余额调整失败');
} finally {
submittingBalance.value = false;
}
};
const fetchUsers = async () => {
loading.value = true;
try {