refactor(dealer): 简化提现功能只支持微信钱包

- 移除支付宝和银行卡提现方式的选择
- 删除相关账户信息输入字段验证逻辑
- 简化提现表单只保留微信钱包选项
- 更新快速金额按钮配置
- 移除多余的状态管理变量
- 删除不再使用的 Radio 和 Cell 组件导入
- 移除提现
This commit is contained in:
2026-01-31 21:14:10 +08:00
parent b9c03be394
commit 3a68955f1c
2 changed files with 43 additions and 335 deletions

View File

@@ -1,184 +0,0 @@
import React from 'react'
import { render, fireEvent, waitFor } from '@testing-library/react'
import DealerWithdraw from '../index'
import { useDealerUser } from '@/hooks/useDealerUser'
import * as withdrawAPI from '@/api/shop/shopDealerWithdraw'
// Mock dependencies
jest.mock('@/hooks/useDealerUser')
jest.mock('@/api/shop/shopDealerWithdraw')
jest.mock('@tarojs/taro', () => ({
showToast: jest.fn(),
getStorageSync: jest.fn(() => 123),
}))
const mockUseDealerUser = useDealerUser as jest.MockedFunction<typeof useDealerUser>
const mockAddShopDealerWithdraw = withdrawAPI.addShopDealerWithdraw as jest.MockedFunction<typeof withdrawAPI.addShopDealerWithdraw>
const mockPageShopDealerWithdraw = withdrawAPI.pageShopDealerWithdraw as jest.MockedFunction<typeof withdrawAPI.pageShopDealerWithdraw>
describe('DealerWithdraw', () => {
const mockDealerUser = {
userId: 123,
money: '10000.00',
realName: '测试用户',
mobile: '13800138000'
}
beforeEach(() => {
mockUseDealerUser.mockReturnValue({
dealerUser: mockDealerUser,
loading: false,
error: null,
refresh: jest.fn()
})
mockPageShopDealerWithdraw.mockResolvedValue({
list: [],
count: 0
})
})
afterEach(() => {
jest.clearAllMocks()
})
test('应该正确显示可提现余额', () => {
const { getByText } = render(<DealerWithdraw />)
expect(getByText('10000.00')).toBeInTheDocument()
expect(getByText('可提现余额')).toBeInTheDocument()
})
test('应该验证最低提现金额', async () => {
mockAddShopDealerWithdraw.mockResolvedValue('success')
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
// 输入低于最低金额的数值
const amountInput = getByPlaceholderText('请输入提现金额')
fireEvent.change(amountInput, { target: { value: '50' } })
// 选择提现方式
const wechatRadio = getByText('微信钱包')
fireEvent.click(wechatRadio)
// 提交表单
const submitButton = getByText('申请提现')
fireEvent.click(submitButton)
await waitFor(() => {
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
title: '最低提现金额为100元',
icon: 'error'
})
})
})
test('应该验证提现金额不超过可用余额', async () => {
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
// 输入超过可用余额的金额
const amountInput = getByPlaceholderText('请输入提现金额')
fireEvent.change(amountInput, { target: { value: '20000' } })
// 选择提现方式
const wechatRadio = getByText('微信钱包')
fireEvent.click(wechatRadio)
// 提交表单
const submitButton = getByText('申请提现')
fireEvent.click(submitButton)
await waitFor(() => {
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
title: '提现金额超过可用余额',
icon: 'error'
})
})
})
test('应该验证支付宝账户信息完整性', async () => {
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
// 输入有效金额
const amountInput = getByPlaceholderText('请输入提现金额')
fireEvent.change(amountInput, { target: { value: '1000' } })
// 选择支付宝提现
const alipayRadio = getByText('支付宝')
fireEvent.click(alipayRadio)
// 只填写账号,不填写姓名
const accountInput = getByPlaceholderText('请输入支付宝账号')
fireEvent.change(accountInput, { target: { value: 'test@alipay.com' } })
// 提交表单
const submitButton = getByText('申请提现')
fireEvent.click(submitButton)
await waitFor(() => {
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
title: '请填写完整的支付宝信息',
icon: 'error'
})
})
})
test('应该成功提交微信提现申请', async () => {
mockAddShopDealerWithdraw.mockResolvedValue('success')
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
// 输入有效金额
const amountInput = getByPlaceholderText('请输入提现金额')
fireEvent.change(amountInput, { target: { value: '1000' } })
// 选择微信提现
const wechatRadio = getByText('微信钱包')
fireEvent.click(wechatRadio)
// 提交表单
const submitButton = getByText('申请提现')
fireEvent.click(submitButton)
await waitFor(() => {
expect(mockAddShopDealerWithdraw).toHaveBeenCalledWith({
userId: 123,
money: '1000',
payType: 10,
applyStatus: 10,
platform: 'MiniProgram'
})
})
await waitFor(() => {
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
title: '提现申请已提交',
icon: 'success'
})
})
})
test('快捷金额按钮应该正常工作', () => {
const { getByText, getByPlaceholderText } = render(<DealerWithdraw />)
// 点击快捷金额按钮
const quickAmountButton = getByText('500')
fireEvent.click(quickAmountButton)
// 验证金额输入框的值
const amountInput = getByPlaceholderText('请输入提现金额') as HTMLInputElement
expect(amountInput.value).toBe('500')
})
test('全部按钮应该设置为可用余额', () => {
const { getByText, getByPlaceholderText } = render(<DealerWithdraw />)
// 点击全部按钮
const allButton = getByText('全部')
fireEvent.click(allButton)
// 验证金额输入框的值
const amountInput = getByPlaceholderText('请输入提现金额') as HTMLInputElement
expect(amountInput.value).toBe('10000.00')
})
})

View File

@@ -1,13 +1,11 @@
import React, {useState, useRef, useEffect, useCallback} from 'react' import React, {useState, useRef, useEffect, useCallback} from 'react'
import {View, Text} from '@tarojs/components' import {View, Text} from '@tarojs/components'
import { import {
Cell,
Space, Space,
Button, Button,
Form, Form,
Input, Input,
CellGroup, CellGroup,
Radio,
Tabs, Tabs,
Tag, Tag,
Empty, Empty,
@@ -89,7 +87,6 @@ const normalizeMoneyString = (money: unknown) => {
const DealerWithdraw: React.FC = () => { const DealerWithdraw: React.FC = () => {
const [activeTab, setActiveTab] = useState<string | number>('0') const [activeTab, setActiveTab] = useState<string | number>('0')
const [selectedAccount, setSelectedAccount] = useState('')
const [loading, setLoading] = useState<boolean>(false) const [loading, setLoading] = useState<boolean>(false)
const [refreshing, setRefreshing] = useState<boolean>(false) const [refreshing, setRefreshing] = useState<boolean>(false)
const [submitting, setSubmitting] = useState<boolean>(false) const [submitting, setSubmitting] = useState<boolean>(false)
@@ -216,14 +213,6 @@ const DealerWithdraw: React.FC = () => {
return return
} }
if (!values.accountType) {
Taro.showToast({
title: '请选择提现方式',
icon: 'error'
})
return
}
// 验证提现金额 // 验证提现金额
const amount = parseFloat(String(values.amount)) const amount = parseFloat(String(values.amount))
const available = parseFloat(normalizeMoneyString(availableAmount).replace(/,/g, '')) const available = parseFloat(normalizeMoneyString(availableAmount).replace(/,/g, ''))
@@ -252,86 +241,48 @@ const DealerWithdraw: React.FC = () => {
return return
} }
// 验证账户信息
if (values.accountType === 'alipay') {
if (!values.account || !values.accountName) {
Taro.showToast({
title: '请填写完整的支付宝信息',
icon: 'error'
})
return
}
} else if (values.accountType === 'bank') {
if (!values.account || !values.accountName || !values.bankName) {
Taro.showToast({
title: '请填写完整的银行卡信息',
icon: 'error'
})
return
}
}
try { try {
setSubmitting(true) setSubmitting(true)
const withdrawData: ShopDealerWithdraw = { const withdrawData: ShopDealerWithdraw = {
userId: dealerUser.userId, userId: dealerUser.userId,
money: values.amount, money: values.amount,
payType: values.accountType === 'wechat' ? 10 : // Only support WeChat wallet withdrawals.
values.accountType === 'alipay' ? 20 : 30, payType: 10,
applyStatus: 10, // 待审核 applyStatus: 10, // 待审核
platform: 'MiniProgram' platform: 'MiniProgram'
} }
// 根据提现方式设置账户信息
if (values.accountType === 'alipay') {
withdrawData.alipayAccount = values.account
withdrawData.alipayName = values.accountName
} else if (values.accountType === 'bank') {
withdrawData.bankCard = values.account
withdrawData.bankAccount = values.accountName
withdrawData.bankName = values.bankName || '银行卡'
}
// WeChat wallet: backend should return `package_info`, frontend opens the "confirm receipt" page // WeChat wallet: backend should return `package_info`, frontend opens the "confirm receipt" page
// for user to click "确认收款". // for user to click "确认收款".
if (values.accountType === 'wechat') { if (!canRequestMerchantTransferConfirm()) {
if (!canRequestMerchantTransferConfirm()) { throw new Error('当前环境不支持微信收款确认,请在微信小程序内操作')
throw new Error('当前环境不支持微信收款确认,请在微信小程序内操作') }
}
const createResult = await addShopDealerWithdraw(withdrawData) const createResult = await addShopDealerWithdraw(withdrawData)
const packageInfo = extractPackageInfo(createResult) const packageInfo = extractPackageInfo(createResult)
if (!packageInfo) { if (!packageInfo) {
throw new Error('后台未返回 package_info无法调起微信收款确认页') throw new Error('后台未返回 package_info无法调起微信收款确认页')
} }
try { try {
await requestMerchantTransferConfirm(packageInfo) 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 || '调起收款确认页失败,请稍后重试')
}
}
} else {
await addShopDealerWithdraw(withdrawData)
Taro.showToast({ Taro.showToast({
title: '提现申请已提交', title: '已调起收款确认页',
icon: 'success' 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 || '调起收款确认页失败,请稍后重试')
}
} }
// 重置表单 // 重置表单
formRef.current?.resetFields() formRef.current?.resetFields()
setSelectedAccount('')
// 刷新数据 // 刷新数据
await handleRefresh() await handleRefresh()
@@ -350,7 +301,7 @@ const DealerWithdraw: React.FC = () => {
} }
} }
const quickAmounts = ['1','100', '300', '500', '1000'] const quickAmounts = ['0.2','100', '300', '500', '1000']
const setQuickAmount = (amount: string) => { const setQuickAmount = (amount: string) => {
formRef.current?.setFieldsValue({amount}) formRef.current?.setFieldsValue({amount})
@@ -409,14 +360,6 @@ const DealerWithdraw: React.FC = () => {
<Input <Input
placeholder="请输入提现金额" placeholder="请输入提现金额"
type="number" type="number"
onChange={(value) => {
// 实时验证提现金额
const amount = parseFloat(String(value))
const available = parseFloat(normalizeMoneyString(availableAmount).replace(/,/g, ''))
if (!isNaN(amount) && amount > available) {
// 可以在这里添加实时提示,但不阻止输入
}
}}
/> />
</Form.Item> </Form.Item>
@@ -444,62 +387,11 @@ const DealerWithdraw: React.FC = () => {
</View> </View>
</View> </View>
<Form.Item name="accountType" label="提现方式" required> <View className="px-4 py-2">
<Radio.Group <Text className="text-sm text-gray-500">
value={selectedAccount}
onChange={(value) => { </Text>
const next = String(value) </View>
setSelectedAccount(next)
// Ensure Form gets the field value even when Radio.Group is controlled.
formRef.current?.setFieldsValue({accountType: next})
}}
>
<Cell.Group>
<Cell>
<Radio value="wechat"></Radio>
</Cell>
<Cell>
<Radio value="alipay"></Radio>
</Cell>
<Cell>
<Radio value="bank"></Radio>
</Cell>
</Cell.Group>
</Radio.Group>
</Form.Item>
{selectedAccount === 'alipay' && (
<>
<Form.Item name="account" label="支付宝账号" required>
<Input placeholder="请输入支付宝账号"/>
</Form.Item>
<Form.Item name="accountName" label="支付宝姓名" required>
<Input placeholder="请输入支付宝实名姓名"/>
</Form.Item>
</>
)}
{selectedAccount === 'bank' && (
<>
<Form.Item name="bankName" label="开户银行" required>
<Input placeholder="请输入开户银行名称"/>
</Form.Item>
<Form.Item name="account" label="银行卡号" required>
<Input placeholder="请输入银行卡号"/>
</Form.Item>
<Form.Item name="accountName" label="开户姓名" required>
<Input placeholder="请输入银行卡开户姓名"/>
</Form.Item>
</>
)}
{selectedAccount === 'wechat' && (
<View className="px-4 py-2">
<Text className="text-sm text-gray-500">
</Text>
</View>
)}
</CellGroup> </CellGroup>
<View className="mt-6 px-4"> <View className="mt-6 px-4">
@@ -508,7 +400,7 @@ const DealerWithdraw: React.FC = () => {
type="primary" type="primary"
nativeType="submit" nativeType="submit"
loading={submitting} loading={submitting}
disabled={submitting || !selectedAccount} disabled={submitting}
> >
{submitting ? '提交中...' : '申请提现'} {submitting ? '提交中...' : '申请提现'}
</Button> </Button>
@@ -532,21 +424,21 @@ const DealerWithdraw: React.FC = () => {
<Text className="text-gray-500 mt-2">...</Text> <Text className="text-gray-500 mt-2">...</Text>
</View> </View>
) : withdrawRecords.length > 0 ? ( ) : withdrawRecords.length > 0 ? (
withdrawRecords.map(record => ( withdrawRecords.map(record => (
<View key={record.id} className="rounded-lg bg-gray-50 p-4 mb-3 shadow-sm"> <View key={record.id} className="rounded-lg bg-gray-50 p-4 mb-3 shadow-sm">
<View className="flex justify-between items-start mb-3"> <View className="flex justify-between items-start mb-3">
<Space> <Space>
<Text className="font-semibold text-gray-800 mb-1"> <Text className="font-semibold text-gray-800 mb-1">
¥{record.money} ¥{record.money}
</Text> </Text>
<Text className="text-sm text-gray-500"> <Text className="text-sm text-gray-500">
{record.accountDisplay} {record.accountDisplay}
</Text> </Text>
</Space> </Space>
<Tag type={getStatusColor(record.applyStatus)}> <Tag type={getStatusColor(record.applyStatus)}>
{getStatusText(record.applyStatus)} {getStatusText(record.applyStatus)}
</Tag> </Tag>
</View> </View>
<View className="text-xs text-gray-400"> <View className="text-xs text-gray-400">
<Text>{record.createTime}</Text> <Text>{record.createTime}</Text>