feat(dealer): 更新提现流程为审核后领取模式

- 添加新的API接口getShopDealerWithdraw和updateShopDealerWithdraw
- 新增package_info相关字段用于微信确认收款流程
- 添加claimingId状态管理用于控制领取按钮
- 修改状态显示逻辑,将"审核通过"改为"待领取",颜色从success改为info
- 移除直接调用微信收款确认的逻辑,改为先提交审核再领取
- 新增handleClaim函数处理提现领取流程
- 在提现记录中添加"立即领取"按钮,仅在待领取状态下显示
- 更新提现说明文案,明确审核后领取流程
- 调整记录列表界面布局,优化时间显示和按钮位置
This commit is contained in:
2026-01-31 22:04:59 +08:00
parent 3a68955f1c
commit 3b98dfa150

View File

@@ -16,11 +16,20 @@ import {Wallet} from '@nutui/icons-react-taro'
import {businessGradients} from '@/styles/gradients'
import Taro from '@tarojs/taro'
import {useDealerUser} from '@/hooks/useDealerUser'
import {pageShopDealerWithdraw, addShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw'
import {
pageShopDealerWithdraw,
addShopDealerWithdraw,
getShopDealerWithdraw,
updateShopDealerWithdraw
} from '@/api/shop/shopDealerWithdraw'
import type {ShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw/model'
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
accountDisplay?: string
// Backend may include these fields for WeChat "confirm receipt" flow after approval.
package_info?: string
packageInfo?: string
package?: string
}
const extractPackageInfo = (result: unknown): string | null => {
@@ -90,6 +99,7 @@ const DealerWithdraw: React.FC = () => {
const [loading, setLoading] = useState<boolean>(false)
const [refreshing, setRefreshing] = useState<boolean>(false)
const [submitting, setSubmitting] = useState<boolean>(false)
const [claimingId, setClaimingId] = useState<number | null>(null)
const [availableAmount, setAvailableAmount] = useState<string>('0.00')
const [withdrawRecords, setWithdrawRecords] = useState<WithdrawRecordWithDetails[]>([])
const formRef = useRef<any>(null)
@@ -179,7 +189,7 @@ const DealerWithdraw: React.FC = () => {
case 40:
return '已到账'
case 20:
return '审核通过'
return '待领取'
case 10:
return '待审核'
case 30:
@@ -194,7 +204,7 @@ const DealerWithdraw: React.FC = () => {
case 40:
return 'success'
case 20:
return 'success'
return 'info'
case 10:
return 'warning'
case 30:
@@ -253,33 +263,12 @@ const DealerWithdraw: React.FC = () => {
platform: 'MiniProgram'
}
// WeChat wallet: backend should return `package_info`, frontend opens the "confirm receipt" page
// for user to click "确认收款".
if (!canRequestMerchantTransferConfirm()) {
throw new Error('当前环境不支持微信收款确认,请在微信小程序内操作')
}
const createResult = await addShopDealerWithdraw(withdrawData)
const packageInfo = extractPackageInfo(createResult)
if (!packageInfo) {
throw new Error('后台未返回 package_info无法调起微信收款确认页')
}
try {
await requestMerchantTransferConfirm(packageInfo)
Taro.showToast({
title: '已调起收款确认页',
icon: 'success'
})
} catch (e: any) {
const msg = String(e?.errMsg || e?.message || '')
if (/cancel/i.test(msg)) {
Taro.showToast({title: '已取消收款确认', icon: 'none'})
} else {
// Keep the original WeChat error for troubleshooting (e.g. "商户号错误").
throw new Error(msg || '调起收款确认页失败,请稍后重试')
}
}
// Security flow:
// 1) user submits => applyStatus=10 (待审核)
// 2) backend审核通过 => applyStatus=20 (待领取)
// 3) user goes to records to "领取" => applyStatus=40 (已到账)
await addShopDealerWithdraw(withdrawData)
Taro.showToast({title: '提现申请已提交,等待审核', icon: 'success'})
// 重置表单
formRef.current?.resetFields()
@@ -301,6 +290,71 @@ const DealerWithdraw: React.FC = () => {
}
}
const handleClaim = async (record: WithdrawRecordWithDetails) => {
if (!record?.id) {
Taro.showToast({title: '记录不存在', icon: 'error'})
return
}
if (record.applyStatus !== 20) {
Taro.showToast({title: '当前状态不可领取', icon: 'none'})
return
}
if (record.payType !== 10) {
Taro.showToast({title: '仅支持微信提现领取', icon: 'none'})
return
}
if (claimingId !== null) return
try {
setClaimingId(record.id)
if (!canRequestMerchantTransferConfirm()) {
throw new Error('当前环境不支持微信收款确认,请在微信小程序内操作')
}
// Prefer getting package from the list record; if missing, query detail.
let packageInfo = extractPackageInfo(record as any)
if (!packageInfo) {
const detail = await getShopDealerWithdraw(record.id)
packageInfo = extractPackageInfo(detail as any)
}
if (!packageInfo) {
throw new Error('后台未返回 package_info无法领取请联系管理员')
}
try {
await requestMerchantTransferConfirm(packageInfo)
} catch (e: any) {
const msg = String(e?.errMsg || e?.message || '')
if (/cancel/i.test(msg)) {
Taro.showToast({title: '已取消领取', icon: 'none'})
return
}
throw new Error(msg || '领取失败,请稍后重试')
}
// Best-effort: ask backend to mark as "已到账".
try {
await updateShopDealerWithdraw({id: record.id, applyStatus: 40} as any)
} catch (e) {
// Backend may enforce state transitions; still refresh to reflect backend truth.
console.warn('更新提现状态失败:', e)
}
Taro.showToast({title: '领取成功', icon: 'success'})
await handleRefresh()
} catch (e: any) {
console.error('领取失败:', e)
Taro.showToast({title: e?.message || '领取失败', icon: 'error'})
} finally {
setClaimingId(null)
}
}
const quickAmounts = ['0.2','100', '300', '500', '1000']
const setQuickAmount = (amount: string) => {
@@ -389,7 +443,7 @@ const DealerWithdraw: React.FC = () => {
<View className="px-4 py-2">
<Text className="text-sm text-gray-500">
</Text>
</View>
</CellGroup>
@@ -431,28 +485,46 @@ const DealerWithdraw: React.FC = () => {
<Text className="font-semibold text-gray-800 mb-1">
¥{record.money}
</Text>
<Text className="text-sm text-gray-500">
{record.accountDisplay}
</Text>
{/*<Text className="text-sm text-gray-500">*/}
{/* 提现账户:{record.accountDisplay}*/}
{/*</Text>*/}
</Space>
<Tag type={getStatusColor(record.applyStatus)}>
<Tag background="#999999" type={getStatusColor(record.applyStatus)} plain>
{getStatusText(record.applyStatus)}
</Tag>
</View>
<View className="text-xs text-gray-400">
<Text>{record.createTime}</Text>
{record.auditTime && (
<Text className="block mt-1">
{new Date(record.auditTime).toLocaleString()}
</Text>
{record.applyStatus === 20 && record.payType === 10 && (
<View className="flex justify-center">
<Button
size="small"
type="primary"
loading={claimingId === record.id}
disabled={claimingId !== null}
onClick={() => handleClaim(record)}
>
</Button>
</View>
)}
{record.rejectReason && (
<Text className="block mt-1 text-red-500">
{record.rejectReason}
</Text>
)}
</View>
<View className="flex justify-between items-center">
<View className="text-xs text-gray-400">
<Text>{record.createTime}</Text>
{record.auditTime && (
<Text className="block mt-1">
{record.auditTime}
</Text>
)}
{record.rejectReason && (
<Text className="block mt-1 text-red-500">
{record.rejectReason}
</Text>
)}
</View>
</View>
</View>
))
) : (