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

@@ -24,7 +24,7 @@ The system SHALL provide a paginated list of all commission withdrawal requests
### Requirement: Withdrawal Request Approval
The system SHALL allow administrators to approve withdrawal requests.
The system SHALL allow administrators to approve withdrawal requests using the approval API contract. The administrator SHALL enter the remark manually, and the system SHALL submit the request's account name, account number, amount, and withdrawal method from the selected list item together with the manual payment type.
#### Scenario: Approve withdrawal request
@@ -32,6 +32,13 @@ The system SHALL allow administrators to approve withdrawal requests.
- **THEN** the system marks the request as approved
- **AND** updates the processor information and timestamp
#### Scenario: Submit approval details
- **WHEN** administrator confirms approval after optionally entering a remark
- **THEN** the system submits the selected request's account name, account number, amount, and withdrawal method with `payment_type` set to `manual`
- **AND** submits the manually entered remark
- **AND** refreshes the list after a successful response
### Requirement: Withdrawal Request Rejection
The system SHALL allow administrators to reject withdrawal requests with a reason.

View File

@@ -41,6 +41,7 @@
- [x] 5.6 Implement approve action with confirmation dialog
- [x] 5.7 Implement reject action with reason input dialog
- [x] 5.8 Add status badges using unified status components
- [x] 5.9 Submit approval request details from the selected list item and collect a manual remark
## 6. Withdrawal Settings Module

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
},
{