feat(admin): 添加实名审核管理页面及提现审核功能

- 新增实名审核管理页面,支持审核通过和驳回操作
- 实现审核记录的分页查询和状态展示
- 添加身份证照片预览功能
- 集成审核状态变更的通知推送
- 在经销商首页添加实名审核入口
- 用户提交实名认证后自动发送审核提醒给管理员
- 经销商提现申请时发送提现审核提醒
- 提现成功后发送到账通知给申请人
- 更新API基础URL配置
- 修复企业认证选项显示问题
- 优化审核流程用户体验
This commit is contained in:
2025-11-20 17:54:09 +08:00
parent d9cd206da8
commit c67b2d4863
9 changed files with 413 additions and 6 deletions

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '实名审核'
})

View File

@@ -0,0 +1,319 @@
import React, {useState, useEffect, useCallback} from 'react'
import {View, Text} from '@tarojs/components'
import {
Space,
Tabs,
Tag,
Empty,
Loading,
PullToRefresh,
Button,
Dialog,
Image,
ImagePreview,
TextArea
} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
import {useDealerUser} from '@/hooks/useDealerUser'
import {pageUserVerify, updateUserVerify} from '@/api/system/userVerify'
import type {ShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw/model'
import {UserVerify} from "@/api/system/userVerify/model";
const UserVeirfyAdmin: React.FC = () => {
const [activeTab, setActiveTab] = useState<string | number>(0)
const [loading, setLoading] = useState<boolean>(false)
const [refreshing, setRefreshing] = useState<boolean>(false)
const [list, setList] = useState<UserVerify[]>([])
const [rejectDialogVisible, setRejectDialogVisible] = useState<boolean>(false)
const [rejectReason, setRejectReason] = useState<string>('')
const [currentRecord, setCurrentRecord] = useState<ShopDealerWithdraw | null>(null)
const [showPreview, setShowPreview] = useState(false)
const [showPreview2, setShowPreview2] = useState(false)
const {dealerUser} = useDealerUser()
// Tab 切换处理函数
const handleTabChange = (value: string | number) => {
console.log('Tab切换到:', value)
setActiveTab(value)
// activeTab变化会自动触发useEffect重新获取数据无需手动调用
}
// 获取审核记录
const fetchWithdrawRecords = useCallback(async () => {
if (!dealerUser?.userId) return
try {
setLoading(true)
const currentStatus = Number(activeTab)
const result = await pageUserVerify({
page: 1,
limit: 100,
status: currentStatus // 后端筛选,提高性能
})
if (result?.list) {
const processedRecords = result.list.map(record => ({
...record
}))
setList(processedRecords)
}
} catch (error) {
console.error('获取审核记录失败:', error)
Taro.showToast({
title: '获取审核记录失败',
icon: 'none'
})
} finally {
setLoading(false)
}
}, [dealerUser?.userId, activeTab])
// 刷新数据
const handleRefresh = async () => {
setRefreshing(true)
await Promise.all([fetchWithdrawRecords()])
setRefreshing(false)
}
// 审核通过
const handleApprove = async (record: ShopDealerWithdraw) => {
try {
await updateUserVerify({
...record,
status: 1, // 审核通过
})
Taro.showToast({
title: '审核通过',
icon: 'success'
})
await fetchWithdrawRecords()
} catch (error: any) {
if (error !== 'cancel') {
console.error('审核通过失败:', error)
Taro.showToast({
title: error.message || '操作失败',
icon: 'none'
})
}
}
}
// 驳回申请
const handleReject = (record: ShopDealerWithdraw) => {
setCurrentRecord(record)
setRejectReason('')
setRejectDialogVisible(true)
}
// 确认驳回
const confirmReject = async () => {
if (!rejectReason.trim()) {
Taro.showToast({
title: '请输入驳回原因',
icon: 'none'
})
return
}
try {
await updateUserVerify({
...currentRecord!,
status: 2, // 驳回
comments: rejectReason.trim()
})
Taro.showToast({
title: '已驳回',
icon: 'success'
})
setRejectDialogVisible(false)
setCurrentRecord(null)
setRejectReason('')
await fetchWithdrawRecords()
} catch (error: any) {
console.error('驳回失败:', error)
Taro.showToast({
title: error.message || '操作失败',
icon: 'none'
})
}
}
// 初始化加载数据
useEffect(() => {
if (dealerUser?.userId) {
fetchWithdrawRecords().then()
}
}, [fetchWithdrawRecords])
const getStatusText = (status?: number) => {
switch (status) {
case 0:
return '待审核'
case 1:
return '审核通过'
case 2:
return '已驳回'
default:
return '未知'
}
}
const getStatusColor = (status?: number) => {
switch (status) {
case 0:
return 'warning'
case 1:
return 'success'
case 2:
return 'danger'
default:
return 'default'
}
}
const renderWithdrawRecords = () => {
console.log('渲染审核记录:', {loading, recordsCount: list.length, dealerUserId: dealerUser?.userId})
return (
<PullToRefresh
disabled={refreshing}
onRefresh={handleRefresh}
>
<View>
{loading ? (
<View className="text-center py-8">
<Loading/>
<Text className="text-gray-500 mt-2">...</Text>
</View>
) : list.length > 0 ? (
list.map(record => (
<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">
<Space direction={'vertical'}>
<Text className="font-semibold text-gray-800">
{record.realName}
</Text>
<Text className="font-normal text-sm text-gray-500">
{record.phone}
</Text>
<Text className="font-normal text-sm text-gray-500">
{record.idCard}
</Text>
</Space>
<Tag type={getStatusColor(record.status)}>
{getStatusText(record.status)}
</Tag>
</View>
<View className="flex gap-2 mb-2">
<Image src={record.sfz1} height={100} onClick={() => setShowPreview(true)}/>
<Image src={record.sfz2} height={100} onClick={() => setShowPreview2(true)}/>
</View>
<ImagePreview
autoPlay
images={[{src: `${record.sfz1}`}]}
visible={showPreview}
onClose={() => setShowPreview(false)}
/>
<ImagePreview
autoPlay
images={[{src: `${record.sfz1}`}]}
visible={showPreview2}
onClose={() => setShowPreview2(false)}
/>
<View className="text-xs text-gray-400">
<Text>{record.createTime}</Text>
{record.status == 1 && (
<Text className="block mt-1">
{record.updateTime}
</Text>
)}
{record.status == 2 && (
<Text className="block mt-1 text-red-500">
{record.comments}
</Text>
)}
</View>
{/* 操作按钮 */}
{record.status === 0 && (
<View className="flex gap-2 mt-3">
<Button
type="success"
size="small"
className="flex-1"
onClick={() => handleApprove(record)}
>
</Button>
<Button
type="danger"
size="small"
className="flex-1"
onClick={() => handleReject(record)}
>
</Button>
</View>
)}
</View>
))
) : (
<Empty description="暂无申请记录"/>
)}
</View>
</PullToRefresh>
)
}
return (
<View className="bg-gray-50 min-h-screen">
<Tabs value={activeTab} onChange={handleTabChange}>
<Tabs.TabPane title="待审核" value="0">
{renderWithdrawRecords()}
</Tabs.TabPane>
<Tabs.TabPane title="已通过" value="1">
{renderWithdrawRecords()}
</Tabs.TabPane>
<Tabs.TabPane title="已驳回" value="2">
{renderWithdrawRecords()}
</Tabs.TabPane>
</Tabs>
{/* 驳回原因对话框 */}
<Dialog
visible={rejectDialogVisible}
title="驳回原因"
onCancel={() => {
setRejectDialogVisible(false)
setCurrentRecord(null)
setRejectReason('')
}}
onConfirm={confirmReject}
>
<View className="p-4">
<TextArea
placeholder="请输入驳回原因"
value={rejectReason}
onChange={(value) => setRejectReason(value)}
maxLength={200}
rows={4}
/>
</View>
</Dialog>
</View>
)
}
export default UserVeirfyAdmin

View File

@@ -1,5 +1,7 @@
import request from '@/utils/request';
import type { ApiResult } from '@/api';
import {UserVerify} from "@/api/system/userVerify/model";
import {ShopDealerWithdraw} from "@/api/shop/shopDealerWithdraw/model";
/**
* 升级为管理员
@@ -14,3 +16,46 @@ export async function pushByUpdateAdmin(userId: number) {
}
return Promise.reject(new Error(res.message));
}
/**
* 通知管理员审核操作提醒
*/
export async function pushReviewReminder(data: UserVerify) {
const res = await request.post<ApiResult<unknown>>(
'/sdy/sdy-template-message/pushReviewReminder',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 通知管理员去提现审核操作提醒
*/
export async function pushWithdrawalReviewReminder(data: ShopDealerWithdraw) {
const res = await request.post<ApiResult<unknown>>(
'/sdy/sdy-template-message/pushWithdrawalReviewReminder',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 提现成功通知
*/
export async function pushNoticeOfWithdrawalToAccount(data: ShopDealerWithdraw) {
const res = await request.post<ApiResult<unknown>>(
'/sdy/sdy-template-message/pushNoticeOfWithdrawalToAccount',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -30,6 +30,8 @@ export interface ShopDealerWithdraw {
rejectReason?: string;
// 来源客户端(APP、H5、小程序等)
platform?: string;
// 手机号
phone?: string;
// 备注
comments?: string;
// 租户id

View File

@@ -88,6 +88,7 @@ export default defineAppConfig({
"pages": [
"index",
"article/index",
"userVerify/index"
]
}
],

View File

@@ -271,6 +271,18 @@ const DealerIndex: React.FC = () => {
)
}
{
(dealerUser?.userId == 33658 || dealerUser?.userId == 33677) && (
<Grid.Item text={'实名审核'} onClick={() => navigateToPage('/admin/userVerify/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-red-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<People color="#10b981" size="20"/>
</View>
</View>
</Grid.Item>
)
}
</Grid>
{/* 第二行功能 */}

View File

@@ -18,6 +18,7 @@ import {pageShopDealerWithdraw, updateShopDealerWithdraw} from '@/api/shop/shopD
import type {ShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw/model'
import {ShopDealerBank} from "@/api/shop/shopDealerBank/model";
import {listShopDealerBank} from "@/api/shop/shopDealerBank";
import {pushNoticeOfWithdrawalToAccount} from "@/api/sdy/sdyTemplateMessage";
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
accountDisplay?: string
@@ -230,6 +231,10 @@ const DealerWithdraw: React.FC = () => {
icon: 'success'
})
if(currentRecord){
await pushNoticeOfWithdrawalToAccount(currentRecord).then()
}
setPayDialogVisible(false)
setCurrentRecord(null)
setPaymentImages([])
@@ -306,6 +311,9 @@ const DealerWithdraw: React.FC = () => {
<Text className="text-sm text-gray-500">
{record.comments}
</Text>
<Text className={'text-sm text-gray-500'}>
{record.bankAccount} {record.phone}
</Text>
</Space>
<Tag type={getStatusColor(record.applyStatus)}>
{getStatusText(record.applyStatus)}
@@ -430,9 +438,18 @@ const DealerWithdraw: React.FC = () => {
onConfirm={confirmPayment}
>
<View className="p-4">
<View className="mb-3">
<View className="mb-3 flex flex-col">
<Text className="text-sm text-gray-600">
¥{Number(currentRecord?.money || 0).toFixed(2)}
¥{(Number(currentRecord?.money) - 3).toFixed(2)}
</Text>
<Text className="text-sm">
{currentRecord?.bankName}
</Text>
<Text className="text-sm">
{currentRecord?.bankAccount}
</Text>
<Text className="text-sm">
{currentRecord?.bankCard}
</Text>
</View>
<View className="mb-3">

View File

@@ -24,6 +24,7 @@ import {listShopDealerBank} from "@/api/shop/shopDealerBank";
import {listCmsWebsiteField} from "@/api/cms/cmsWebsiteField";
import {myUserVerify} from "@/api/system/userVerify";
import navTo from "@/utils/common";
import {pushWithdrawalReviewReminder} from "@/api/sdy/sdyTemplateMessage";
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
accountDisplay?: string
@@ -278,7 +279,7 @@ const DealerWithdraw: React.FC = () => {
icon: 'success'
})
await
await pushWithdrawalReviewReminder(withdrawData)
// 重置表单
setWithdrawAmount('')

View File

@@ -12,6 +12,7 @@ import {
import {UserVerify} from "@/api/system/userVerify/model";
import {addUserVerify, myUserVerify, updateUserVerify} from "@/api/system/userVerify";
import {uploadFile} from "@/api/system/file";
import {pushReviewReminder} from "@/api/sdy/sdyTemplateMessage";
function Index() {
const [isUpdate, setIsUpdate] = useState<boolean>(false)
@@ -95,6 +96,12 @@ function Index() {
const saveOrUpdate = isUpdate ? updateUserVerify : addUserVerify;
saveOrUpdate({...FormData, status: 0}).then(() => {
Taro.showToast({title: `提交成功`, icon: 'success'})
if(!isUpdate){
// 发送派单成功提醒 0FBKFCWXe8WyjReYXwSDEXf1-pxYKQXE0quZre3GYIM
pushReviewReminder({
realName: FormData.realName,
}).then()
}
setTimeout(() => {
return Taro.navigateBack()
}, 1000)
@@ -185,9 +192,9 @@ function Index() {
<Radio key={0} value={0}>
</Radio>
<Radio key={1} value={1}>
</Radio>
{/*<Radio key={1} value={1}>*/}
{/* 企业*/}
{/*</Radio>*/}
</Radio.Group>
</Form.Item>
{