fix: 提现审批
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 9m2s

This commit is contained in:
luo
2026-09-29 10:16:33 +08:00
parent 8229ad9248
commit 330affe0c9
9 changed files with 158 additions and 38 deletions

View File

@@ -8,6 +8,7 @@ import type {
WithdrawalRequestQueryParams,
WithdrawalRequestPageResult,
ApproveWithdrawalParams,
WithdrawalApprovalResponse,
RejectWithdrawalParams,
WithdrawalSettingListResult,
WithdrawalSettingItem,
@@ -46,8 +47,11 @@ export class CommissionService extends BaseService {
* 审批通过提现申请
* POST /api/admin/commission/withdrawal-requests/{id}/approve
*/
static approveWithdrawal(id: number, params?: ApproveWithdrawalParams): Promise<BaseResponse> {
return this.post<BaseResponse>(
static approveWithdrawal(
id: number,
params: ApproveWithdrawalParams
): Promise<BaseResponse<WithdrawalApprovalResponse>> {
return this.post<BaseResponse<WithdrawalApprovalResponse>>(
`/api/admin/commission/withdrawal-requests/${id}/approve`,
params
)

View File

@@ -88,7 +88,23 @@ export interface WithdrawalRequestQueryParams extends PaginationParams {
* 审批提现申请参数
*/
export interface ApproveWithdrawalParams {
account_name?: string | null // 修正后的收款人姓名
account_number?: string | null // 修正后的收款账号
amount?: number | null // 修正后的提现金额(分)
payment_type: PaymentType // 放款类型
remark?: string // 审批备注
withdrawal_method?: WithdrawalMethod | null // 修正后的收款类型
}
/**
* 审批提现申请响应
*/
export interface WithdrawalApprovalResponse {
id?: number // 提现申请ID
processed_at?: string // 处理时间
status?: WithdrawalStatus // 状态
status_name?: string // 状态名称
withdrawal_no?: string // 提现单号
}
/**

View File

@@ -93,11 +93,9 @@ declare module 'vue' {
ElAlert: typeof import('element-plus/es')['ElAlert']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButton: typeof import('element-plus/es')['ElButton']
ElCalendar: typeof import('element-plus/es')['ElCalendar']
ElCard: typeof import('element-plus/es')['ElCard']
ElCascader: typeof import('element-plus/es')['ElCascader']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElCol: typeof import('element-plus/es')['ElCol']
ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
@@ -115,6 +113,7 @@ declare module 'vue' {
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElMenu: typeof import('element-plus/es')['ElMenu']
@@ -122,13 +121,13 @@ declare module 'vue' {
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElResult: typeof import('element-plus/es')['ElResult']
ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']

View File

@@ -592,7 +592,8 @@
{
prop: 'username',
label: '账号名称',
minWidth: 120
minWidth: 160,
showOverflowTooltip: true
},
{
prop: 'phone',
@@ -617,6 +618,7 @@
prop: 'shop_name',
label: '店铺名称',
minWidth: 150,
showOverflowTooltip: true,
formatter: (row: any) => {
return row.shop_name || '-'
}

View File

@@ -7,6 +7,7 @@ import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { AssetService } from '@/api/modules'
import { FrontendRateLimitError } from '@/utils/business/apiRateLimit'
import { normalizeApiError } from '@/utils/business/apiError'
import type {
AssetBoundCard,
AssetWalletResponse,
@@ -32,6 +33,53 @@ const NULLABLE_REALTIME_CARD_FIELDS = new Set([
// 详情接口是绑定卡实名时间的唯一来源;实时状态接口不应覆盖或保留旧值。
const DETAIL_ONLY_BOUND_CARD_FIELDS = new Set(['real_name_at'])
const ASSET_NOT_FOUND_CODE = 1006
const isLegacyNineteenDigitIdentifier = (identifier: string) => /^89\d{17}$/.test(identifier)
const isAssetNotFound = (value: any) =>
value?.code === ASSET_NOT_FOUND_CODE || value?.response?.data?.code === ASSET_NOT_FOUND_CODE
const isUnauthorized = (value: any) =>
value?.code === 401 || value?.response?.status === 401 || value?.response?.data?.code === 401
const calculateLuhnCheckDigit = (identifier: string) => {
let sum = 0
let doubleDigit = true
for (let index = identifier.length - 1; index >= 0; index -= 1) {
let digit = Number(identifier[index])
if (doubleDigit) {
digit *= 2
if (digit > 9) digit -= 9
}
sum += digit
doubleDigit = !doubleDigit
}
return String((10 - (sum % 10)) % 10)
}
const resolveAssetForAssetInfo = async (identifier: string) => {
const params = { include_usage_summary: true }
if (!isLegacyNineteenDigitIdentifier(identifier)) {
return AssetService.resolveAsset(identifier, params)
}
const silentConfig = { requestOptions: { errorMessageMode: 'none' as const } }
let firstResponse = null
try {
firstResponse = await AssetService.resolveAsset(identifier, params, silentConfig)
} catch (error) {
if (!isAssetNotFound(error)) throw error
}
if (firstResponse && !isAssetNotFound(firstResponse)) return firstResponse
const correctedIdentifier = `${identifier}${calculateLuhnCheckDigit(identifier)}`
return AssetService.resolveAsset(correctedIdentifier, params, silentConfig)
}
/**
* 格式化数据大小(MB -> GB/MB)
@@ -96,9 +144,10 @@ export function useAssetInfo() {
* @param identifier - 资产标识符(ICCID、IMEI、SN、虚拟号或MSISDN)
*/
const fetchAssetDetail = async (identifier: string) => {
const shouldShowSilentRequestError = isLegacyNineteenDigitIdentifier(identifier)
try {
loading.value = true
const response = await AssetService.resolveAsset(identifier, { include_usage_summary: true })
const response = await resolveAssetForAssetInfo(identifier)
if (response.code === 0 && response.data) {
const data = response.data
@@ -199,8 +248,12 @@ export function useAssetInfo() {
ElMessage.error(response.msg || '查询失败')
cardInfo.value = null
}
} catch {
} catch (error: any) {
cardInfo.value = null
if (shouldShowSilentRequestError && !isUnauthorized(error)) {
const backendMessage = error?.response?.data?.msg
ElMessage.error(backendMessage || normalizeApiError(error).message)
}
} finally {
loading.value = false
}

View File

@@ -409,12 +409,14 @@
// fetchAssetDetail 内部已经会并行加载所有相关数据,不需要重复调用
await fetchAssetDetail(identifier)
if (!cardInfo.value) return
// 如果是设备类型,需要再次调用搜索接口获取完整数据
if (cardInfo.value?.asset_type === 'device') {
const deviceIdentifier = cardInfo.value.virtual_no || cardInfo.value.iccid || identifier
await fetchAssetDetail(deviceIdentifier)
}
if (!cardInfo.value) return
if (cardInfo.value) {
walletTransactionSearchToken.value += 1
@@ -428,7 +430,8 @@
// 重置分页并加载套餐列表
packagePagination.value.page = 1
const result = await loadPackageList(identifier, 1, packagePagination.value.pageSize)
const resolvedIdentifier = cardInfo.value.identifier || identifier
const result = await loadPackageList(resolvedIdentifier, 1, packagePagination.value.pageSize)
packagePagination.value.total = result.total
}

View File

@@ -69,6 +69,30 @@
</div>
</template>
</ElDialog>
<!-- 审批通过对话框 -->
<ElDialog v-model="approveDialogVisible" title="审批通过提现申请" width="40%">
<ElForm :model="approveForm">
<ElFormItem label="备注信息" prop="remark">
<ElInput
v-model="approveForm.remark"
type="textarea"
:rows="3"
maxlength="500"
show-word-limit
placeholder="请输入备注信息(选填)"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="approveDialogVisible = false">{{ $t('common.cancel') }}</ElButton>
<ElButton type="primary" @click="handleApproveSubmit" :loading="approveSubmitLoading">
{{ $t('common.confirm') }}
</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
@@ -77,28 +101,30 @@
<script setup lang="ts">
import { h } from 'vue'
import { CommissionService, ShopService } from '@/api/modules'
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
import { ElMessage, ElTag } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type {
WithdrawalRequestItem,
WithdrawalStatus,
WithdrawalMethod
} from '@/types/api/commission'
import { PaymentType } from '@/types/api/commission'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { formatDateTime, formatMoney } from '@/utils/business/format'
import { WithdrawalStatusMap, WithdrawalMethodMap } from '@/config/constants'
import { useI18n } from 'vue-i18n'
defineOptions({ name: 'WithdrawalApproval' })
const { t } = useI18n() // 仅保留用于 common.tips, common.confirm, common.cancel
const rejectDialogVisible = ref(false)
const approveDialogVisible = ref(false)
const loading = ref(false)
const approveSubmitLoading = ref(false)
const rejectSubmitLoading = ref(false)
const tableRef = ref()
const currentWithdrawalId = ref<number>(0)
const currentWithdrawal = ref<WithdrawalRequestItem | null>(null)
const approveForm = reactive({ remark: '' })
// 店铺选项
const shopOptions = ref<any[]>([])
@@ -216,7 +242,8 @@
{
prop: 'withdrawal_no',
label: '提现单号',
minWidth: 180
minWidth: 180,
showOverflowTooltip: true
},
{
prop: 'shop_name',
@@ -232,21 +259,18 @@
prop: 'amount',
label: '提现金额',
width: 120,
align: 'right',
formatter: (row: WithdrawalRequestItem) => formatMoney(row.amount)
},
{
prop: 'fee',
label: '手续费',
width: 100,
align: 'right',
formatter: (row: WithdrawalRequestItem) => formatMoney(row.fee)
},
{
prop: 'actual_amount',
label: '实际到账',
width: 120,
align: 'right',
formatter: (row: WithdrawalRequestItem) => formatMoney(row.actual_amount)
},
{
@@ -367,25 +391,36 @@
getTableData()
}
// 审批通过
const handleApprove = (row: WithdrawalRequestItem) => {
ElMessageBox.confirm('确定要通过该提现申请吗?', t('common.tips'), {
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
type: 'warning'
})
.then(async () => {
try {
await CommissionService.approveWithdrawal(row.id)
ElMessage.success('审批通过成功')
getTableData()
} catch (error) {
console.error(error)
}
})
.catch(() => {
// 用户取消
// 显示审批通过对话框
const showApproveDialog = (row: WithdrawalRequestItem) => {
currentWithdrawal.value = row
approveForm.remark = ''
approveDialogVisible.value = true
}
// 提交审批通过
const handleApproveSubmit = async () => {
const row = currentWithdrawal.value
if (!row) return
approveSubmitLoading.value = true
try {
await CommissionService.approveWithdrawal(row.id, {
account_name: row.account_name,
account_number: row.account_number,
amount: row.amount,
payment_type: PaymentType.MANUAL,
remark: approveForm.remark,
withdrawal_method: row.withdrawal_method
})
ElMessage.success('审批通过成功')
approveDialogVisible.value = false
getTableData()
} catch (error) {
console.error(error)
} finally {
approveSubmitLoading.value = false
}
}
// 显示拒绝对话框
@@ -428,7 +463,7 @@
return [
{
label: '审批通过',
handler: () => handleApprove(row),
handler: () => showApproveDialog(row),
type: 'primary' as const
},
{