feat: align withdrawal management with admin prototype
| | |
| | | export function updateAdminStaff(id: number | string, payload: Omit<AdminStaff, 'id' | 'createdAt'> & { password?: string }) { return http.put<AdminStaff, AdminStaff>(`/api/admin/management/staff/${id}`, payload) } |
| | | export function resetAdminStaffPassword(id: number | string, password: string) { return http.patch<any, any>(`/api/admin/management/staff/${id}/password`, { password }) } |
| | | export function deleteAdminStaff(id: number | string) { return http.delete<any, any>(`/api/admin/management/staff/${id}`) } |
| | | export function getMerchantSettlement(kind: 'onboardings' | 'bank-cards' | 'withdrawals') { return http.get<PageResult<any>, PageResult<any>>(`/api/admin/management/merchant-settlement/${kind}`) } |
| | | export interface MerchantSettlementQuery { |
| | | page?: number |
| | | size?: number |
| | | merchantName?: string |
| | | contactMobile?: string |
| | | merchantType?: string |
| | | status?: string |
| | | startDate?: string |
| | | endDate?: string |
| | | } |
| | | export function getMerchantSettlement(kind: 'onboardings' | 'bank-cards' | 'withdrawals', params: MerchantSettlementQuery = {}) { return http.get<PageResult<any>, PageResult<any>>(`/api/admin/management/merchant-settlement/${kind}`, { params }) } |
| | | export function reviewMerchantSettlement(kind: 'onboardings' | 'bank-cards' | 'withdrawals', id: number | string, status: string, reason = '', providerValue = '') { const body: Record<string, string> = { status, reason }; if (kind === 'onboardings') body.huifuId = providerValue; if (kind === 'bank-cards') body.tokenNo = providerValue; return http.patch<any, any>(`/api/admin/management/merchant-settlement/${kind}/${id}`, body) } |
| | | |
| | | export function getAdminNews(params: NewsArticleQuery = {}) { |
| | |
| | | ] |
| | | |
| | | const financeMenus: MenuItem[] = [ |
| | | { path: '/merchant-settlement', label: '商户结算审核' }, |
| | | { path: '/merchant-settlement', label: '提现管理' }, |
| | | ] |
| | | |
| | | const orderMenus: MenuItem[] = [ |
| | |
| | | path: 'merchant-settlement', |
| | | name: 'merchant-settlement', |
| | | component: () => import('../views/MerchantSettlementView.vue'), |
| | | meta: { title: '商户结算审核' }, |
| | | meta: { title: '提现管理' }, |
| | | }, |
| | | { |
| | | path: 'products', |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, onMounted, reactive, ref } from 'vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { Refresh, Search } from '@element-plus/icons-vue' |
| | | import { getMerchantSettlement, reviewMerchantSettlement } from '../api/admin' |
| | | |
| | | type Kind = 'onboardings' | 'bank-cards' | 'withdrawals' |
| | | const kind = ref<Kind>('onboardings'), loading = ref(false), rows = ref<any[]>([]) |
| | | const labels: Record<string,string> = { pending: '待审核', approved: '已通过', rejected: '已驳回', processing: '取现处理中', paid: '已到账', failed: '失败' } |
| | | const filters = reactive({ merchantName: '', applicationNo: '', status: '' }) |
| | | const filteredRows = computed(() => rows.value.filter((row) => { |
| | | const merchantMatched = !filters.merchantName || String(row.merchantName || '').includes(filters.merchantName.trim()) |
| | | const applicationMatched = !filters.applicationNo || String(row.applicationNo || '').includes(filters.applicationNo.trim()) |
| | | return merchantMatched && applicationMatched && (!filters.status || row.status === filters.status) |
| | | })) |
| | | const load = async () => { loading.value = true; try { rows.value = (await getMerchantSettlement(kind.value)).records || [] } finally { loading.value = false } } |
| | | const resetFilters = () => { filters.merchantName = ''; filters.applicationNo = ''; filters.status = '' } |
| | | const review = async (row: any, status: string) => { |
| | | let reason = '', providerValue = '' |
| | | if (status === 'approved') { const isOnboarding = kind.value === 'onboardings'; const result = await ElMessageBox.prompt(isOnboarding ? '请输入汇付进件成功后返回的商户 huifu_id' : '请输入汇付绑卡成功后返回的取现卡 token_no', isOnboarding ? '配置汇付商户号' : '配置汇付取现卡序列号', { inputPattern: /^[A-Za-z0-9]{1,32}$/, inputErrorMessage: '请输入有效的汇付标识' }); providerValue = result.value } |
| | | else if (status === 'rejected' || status === 'failed') { const result = await ElMessageBox.prompt('请输入处理原因', '确认处理'); reason = result.value || '' } |
| | | else { await ElMessageBox.confirm('确认发起汇付取现?取现结果将以汇付同步返回或签名回调为准。', '发起取现', { type: 'warning' }) } |
| | | await reviewMerchantSettlement(kind.value, row.id, status, reason, providerValue); ElMessage.success(status === 'paid' ? '已提交汇付取现' : '处理成功'); load() |
| | | |
| | | const kind = ref<Kind>('withdrawals') |
| | | const loading = ref(false) |
| | | const rows = ref<Record<string, any>[]>([]) |
| | | const total = ref(0) |
| | | const page = ref(1) |
| | | const pageSize = ref(20) |
| | | const reviewVisible = ref(false) |
| | | const reviewing = ref<Record<string, any> | null>(null) |
| | | |
| | | const filters = reactive({ |
| | | merchantName: '', |
| | | contactMobile: '', |
| | | merchantType: '', |
| | | status: '', |
| | | dateRange: [] as string[], |
| | | }) |
| | | |
| | | const tabOptions: Array<{ value: Kind; label: string }> = [ |
| | | { value: 'withdrawals', label: '提现管理' }, |
| | | { value: 'onboardings', label: '商户进件' }, |
| | | { value: 'bank-cards', label: '银行卡申请' }, |
| | | ] |
| | | |
| | | const merchantTypes = ['餐饮服务', '住宿服务', '购房服务', '购车服务', '体检服务', '金融服务', '行政审批', '景区服务'] |
| | | const statusOptions = computed(() => kind.value === 'withdrawals' |
| | | ? [ |
| | | { value: 'pending', label: '待审核' }, |
| | | { value: 'processing', label: '处理中' }, |
| | | { value: 'paid', label: '已到账' }, |
| | | { value: 'failed', label: '提现失败' }, |
| | | ] |
| | | : [ |
| | | { value: 'pending', label: '待审核' }, |
| | | { value: 'approved', label: '已通过' }, |
| | | { value: 'rejected', label: '已驳回' }, |
| | | ]) |
| | | |
| | | function statusLabel(value: string) { |
| | | return ({ pending: '待审核', approved: '已通过', rejected: '已驳回', processing: '处理中', paid: '已到账', failed: '提现失败' } as Record<string, string>)[value] || value || '-' |
| | | } |
| | | |
| | | function statusType(value: string) { |
| | | return ({ pending: 'warning', approved: 'success', paid: 'success', rejected: 'danger', failed: 'danger', processing: 'primary' } as Record<string, 'warning' | 'success' | 'danger' | 'primary'>)[value] || 'primary' |
| | | } |
| | | |
| | | function formatAmount(value: unknown) { |
| | | if (value === null || value === undefined || value === '') return '-' |
| | | return `¥${(Number(value) / 100).toFixed(2)}` |
| | | } |
| | | |
| | | function formatDate(value: unknown) { |
| | | if (!value) return '-' |
| | | return String(value).replace('T', ' ').replace(/\.\d+$/, '') |
| | | } |
| | | |
| | | async function load() { |
| | | loading.value = true |
| | | try { |
| | | const result = await getMerchantSettlement(kind.value, { |
| | | page: page.value, |
| | | size: pageSize.value, |
| | | merchantName: filters.merchantName.trim() || undefined, |
| | | contactMobile: filters.contactMobile.trim() || undefined, |
| | | merchantType: filters.merchantType || undefined, |
| | | status: filters.status || undefined, |
| | | startDate: filters.dateRange[0], |
| | | endDate: filters.dateRange[1], |
| | | }) |
| | | rows.value = result.records || [] |
| | | total.value = Number(result.total || 0) |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | function search() { |
| | | page.value = 1 |
| | | load() |
| | | } |
| | | |
| | | function reset() { |
| | | filters.merchantName = '' |
| | | filters.contactMobile = '' |
| | | filters.merchantType = '' |
| | | filters.status = '' |
| | | filters.dateRange = [] |
| | | page.value = 1 |
| | | load() |
| | | } |
| | | |
| | | function changeKind(value: string | number | boolean | undefined) { |
| | | kind.value = value as Kind |
| | | page.value = 1 |
| | | filters.status = '' |
| | | load() |
| | | } |
| | | |
| | | function openReview(row: Record<string, any>) { |
| | | reviewing.value = row |
| | | reviewVisible.value = true |
| | | } |
| | | |
| | | async function review(status: 'paid' | 'failed' | 'approved' | 'rejected') { |
| | | const row = reviewing.value |
| | | if (!row) return |
| | | let reason = '' |
| | | let providerValue = '' |
| | | try { |
| | | if (status === 'paid') { |
| | | await ElMessageBox.confirm('确认发起该笔提现的汇付代付?代付结果以汇付同步响应或签名回调为准。', '确认审核通过', { type: 'warning' }) |
| | | } else if (status === 'approved') { |
| | | const onboarding = kind.value === 'onboardings' |
| | | const result = await ElMessageBox.prompt( |
| | | onboarding ? '请输入汇付进件成功后返回的商户 huifu_id' : '请输入汇付绑卡成功后返回的提现卡 token_no', |
| | | onboarding ? '配置汇付商户号' : '配置汇付提现卡序列号', |
| | | { inputPattern: /^[A-Za-z0-9]{1,32}$/, inputErrorMessage: '请输入有效的汇付标识' }, |
| | | ) |
| | | providerValue = result.value |
| | | } else { |
| | | const result = await ElMessageBox.prompt('请输入处理原因', '驳回申请', { |
| | | inputValidator: (value) => value.trim() ? true : '请填写驳回原因', |
| | | }) |
| | | reason = result.value.trim() |
| | | } |
| | | await reviewMerchantSettlement(kind.value, row.id, status, reason, providerValue) |
| | | ElMessage.success(status === 'paid' ? '已提交汇付代付' : '审核处理成功') |
| | | reviewVisible.value = false |
| | | load() |
| | | } catch { |
| | | // Element Plus confirmation and prompt cancellations do not need a user-facing error. |
| | | } |
| | | } |
| | | |
| | | function handlePageChange(value: number) { |
| | | page.value = value |
| | | load() |
| | | } |
| | | |
| | | onMounted(load) |
| | | </script> |
| | | <template><div class="page-stack"><el-card><div class="toolbar"><el-radio-group v-model="kind" @change="load"><el-radio-button label="onboardings">商户进件</el-radio-button><el-radio-button label="bank-cards">银行卡审核</el-radio-button><el-radio-button label="withdrawals">提现审核</el-radio-button></el-radio-group><el-button @click="load">刷新</el-button></div><el-form class="settlement-filters" inline><el-form-item label="商户"><el-input v-model="filters.merchantName" clearable placeholder="请输入商户名称" /></el-form-item><el-form-item label="申请单号"><el-input v-model="filters.applicationNo" clearable placeholder="请输入申请单号" /></el-form-item><el-form-item label="状态"><el-select v-model="filters.status" clearable placeholder="全部"><el-option label="待审核" value="pending" /><el-option label="已通过" value="approved" /><el-option label="已驳回" value="rejected" /><el-option label="处理中" value="processing" /><el-option label="已到账" value="paid" /><el-option label="失败" value="failed" /></el-select></el-form-item><el-form-item><el-button @click="resetFilters">重置</el-button></el-form-item></el-form><el-table v-loading="loading" :data="filteredRows" border><el-table-column prop="merchantName" label="商户"/><el-table-column prop="storeName" label="门店"/><el-table-column prop="applicationNo" label="申请单号"/><el-table-column prop="bankName" label="银行"/><el-table-column label="金额"><template #default="{row}">{{row.amountCents == null ? '-' : '¥' + (Number(row.amountCents)/100).toFixed(2)}}</template></el-table-column><el-table-column label="状态"><template #default="{row}">{{labels[row.status] || row.status}}</template></el-table-column><el-table-column label="操作" width="180"><template #default="{row}"><template v-if="row.status==='pending'"><el-button size="small" type="primary" @click="review(row,kind==='withdrawals'?'paid':'approved')">{{kind==='withdrawals'?'发起取现':'通过'}}</el-button><el-button size="small" type="danger" @click="review(row,kind==='withdrawals'?'failed':'rejected')">驳回</el-button></template></template></el-table-column></el-table></el-card></div></template> |
| | | <style scoped>.toolbar{display:flex;justify-content:space-between;margin-bottom:18px}.settlement-filters{padding:14px 0 4px}</style> |
| | | |
| | | <template> |
| | | <div class="withdrawal-page"> |
| | | <div class="settlement-tabs"> |
| | | <el-radio-group v-model="kind" @change="changeKind"> |
| | | <el-radio-button v-for="tab in tabOptions" :key="tab.value" :label="tab.value"> |
| | | {{ tab.label }} |
| | | </el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | |
| | | <section class="query-panel"> |
| | | <div class="panel-title">查询表格</div> |
| | | <el-form class="query-form" label-width="70px" @submit.prevent="search"> |
| | | <el-form-item label="商家"> |
| | | <el-input v-model="filters.merchantName" clearable placeholder="请输入商家名称" /> |
| | | </el-form-item> |
| | | <el-form-item label="联系电话"> |
| | | <el-input v-model="filters.contactMobile" clearable placeholder="请输入商家联系电话" /> |
| | | </el-form-item> |
| | | <el-form-item label="申请时间"> |
| | | <el-date-picker |
| | | v-model="filters.dateRange" |
| | | type="daterange" |
| | | value-format="YYYY-MM-DD" |
| | | range-separator="至" |
| | | start-placeholder="开始日期" |
| | | end-placeholder="结束日期" |
| | | unlink-panels |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="商家类型"> |
| | | <el-select v-model="filters.merchantType" clearable placeholder="全部"> |
| | | <el-option v-for="item in merchantTypes" :key="item" :label="item" :value="item" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="状态"> |
| | | <el-select v-model="filters.status" clearable placeholder="全部"> |
| | | <el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item class="query-actions"> |
| | | <el-button type="primary" :icon="Search" @click="search">查询</el-button> |
| | | <el-button :icon="Refresh" @click="reset">重置</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </section> |
| | | |
| | | <section class="table-panel"> |
| | | <el-table v-loading="loading" :data="rows" border class="settlement-table" empty-text="暂无数据"> |
| | | <template v-if="kind === 'withdrawals'"> |
| | | <el-table-column prop="withdrawalNo" label="提现Id" min-width="156" /> |
| | | <el-table-column prop="merchantName" label="商家名称" min-width="130" show-overflow-tooltip /> |
| | | <el-table-column prop="contactMobile" label="联系方式" min-width="118" /> |
| | | <el-table-column prop="merchantType" label="商家类型" min-width="110" /> |
| | | <el-table-column label="可提现金额" min-width="116" align="right"> |
| | | <template #default="{ row }">{{ formatAmount(row.availableAmountCents) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="提现金额" min-width="104" align="right"> |
| | | <template #default="{ row }">{{ formatAmount(row.amountCents) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="bankName" label="提现银行" min-width="130" show-overflow-tooltip /> |
| | | <el-table-column prop="cardMasked" label="提现卡号" min-width="150" /> |
| | | </template> |
| | | <template v-else-if="kind === 'onboardings'"> |
| | | <el-table-column prop="applicationNo" label="进件申请Id" min-width="156" /> |
| | | <el-table-column prop="merchantName" label="商家名称" min-width="150" show-overflow-tooltip /> |
| | | <el-table-column prop="contactMobile" label="联系方式" min-width="118" /> |
| | | <el-table-column prop="merchantType" label="商家类型" min-width="110" /> |
| | | <el-table-column prop="subjectName" label="进件主体" min-width="150" show-overflow-tooltip /> |
| | | <el-table-column prop="subjectType" label="主体类型" min-width="105" /> |
| | | </template> |
| | | <template v-else> |
| | | <el-table-column prop="id" label="申请Id" min-width="100" /> |
| | | <el-table-column prop="merchantName" label="商家名称" min-width="150" show-overflow-tooltip /> |
| | | <el-table-column prop="contactMobile" label="联系方式" min-width="118" /> |
| | | <el-table-column prop="merchantType" label="商家类型" min-width="110" /> |
| | | <el-table-column prop="bankName" label="开户银行" min-width="145" show-overflow-tooltip /> |
| | | <el-table-column prop="branchName" label="开户支行" min-width="180" show-overflow-tooltip /> |
| | | <el-table-column prop="cardMasked" label="银行卡号" min-width="150" /> |
| | | </template> |
| | | <el-table-column label="申请时间" min-width="165"> |
| | | <template #default="{ row }">{{ formatDate(row.createdAt) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="状态" min-width="100" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="statusType(row.status)" effect="light">{{ statusLabel(row.status) }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="90" fixed="right" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-button v-if="row.status === 'pending'" link type="primary" @click="openReview(row)">审核</el-button> |
| | | <span v-else class="muted">-</span> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div class="pagination-bar"> |
| | | <span>共 {{ total }} 条</span> |
| | | <el-pagination |
| | | v-model:current-page="page" |
| | | v-model:page-size="pageSize" |
| | | background |
| | | layout="prev, pager, next" |
| | | :total="total" |
| | | @current-change="handlePageChange" |
| | | /> |
| | | </div> |
| | | </section> |
| | | |
| | | <el-dialog v-model="reviewVisible" title="提现审核" width="400px" :close-on-click-modal="false"> |
| | | <template v-if="reviewing"> |
| | | <div class="review-summary"> |
| | | <div><span>商家名称</span><strong>{{ reviewing.merchantName || '-' }}</strong></div> |
| | | <div><span>提现金额</span><strong>{{ formatAmount(reviewing.amountCents) }}</strong></div> |
| | | <div><span>提现银行</span><strong>{{ reviewing.bankName || '-' }} {{ reviewing.cardMasked || '' }}</strong></div> |
| | | </div> |
| | | </template> |
| | | <template #footer> |
| | | <el-button @click="reviewVisible = false">取消</el-button> |
| | | <el-button type="danger" plain @click="review(kind === 'withdrawals' ? 'failed' : 'rejected')">驳回</el-button> |
| | | <el-button type="primary" @click="review(kind === 'withdrawals' ? 'paid' : 'approved')">审核通过</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .withdrawal-page { min-width: 1040px; } |
| | | .settlement-tabs { margin: 0 0 16px; } |
| | | .settlement-tabs :deep(.el-radio-button__inner) { min-width: 96px; } |
| | | .query-panel, .table-panel { background: #fff; border: 1px solid #edf0f5; } |
| | | .query-panel { padding: 18px 20px 6px; } |
| | | .panel-title { margin-bottom: 16px; color: #1d2129; font-size: 15px; font-weight: 600; } |
| | | .query-form { display: grid; grid-template-columns: repeat(3, minmax(250px, 1fr)); column-gap: 24px; } |
| | | .query-form :deep(.el-form-item) { margin-bottom: 16px; } |
| | | .query-form :deep(.el-form-item__content) { min-width: 0; } |
| | | .query-form :deep(.el-input), .query-form :deep(.el-select), .query-form :deep(.el-date-editor) { width: 100%; } |
| | | .query-actions { justify-content: flex-end; } |
| | | .query-actions :deep(.el-form-item__content) { justify-content: flex-end; } |
| | | .table-panel { margin-top: 16px; padding: 0 20px 12px; } |
| | | .settlement-table { width: 100%; } |
| | | .settlement-table :deep(th.el-table__cell) { color: #303133; font-weight: 600; background: #f7f8fa; } |
| | | .settlement-table :deep(.el-table__cell) { padding: 10px 0; } |
| | | .pagination-bar { display: flex; align-items: center; justify-content: flex-end; gap: 18px; padding-top: 14px; color: #606266; font-size: 13px; } |
| | | .muted { color: #909399; } |
| | | .review-summary { display: grid; gap: 14px; } |
| | | .review-summary div { display: flex; justify-content: space-between; gap: 16px; } |
| | | .review-summary span { color: #909399; } |
| | | .review-summary strong { color: #303133; font-weight: 500; text-align: right; } |
| | | @media (max-width: 1280px) { .query-form { grid-template-columns: repeat(2, minmax(280px, 1fr)); } } |
| | | </style> |
| | |
| | | @PutMapping("/staff/{id}") public ApiResponse<Map<String,Object>> updateStaff(@PathVariable Long id, @Valid @RequestBody AdminStaffRequest request) { return ApiResponse.success(service.updateStaff(id, request)); } |
| | | @PatchMapping("/staff/{id}/password") public ApiResponse<Map<String,Object>> resetStaffPassword(@PathVariable Long id, @RequestBody Map<String,String> body) { return ApiResponse.success(service.resetStaffPassword(id, body.get("password"))); } |
| | | @DeleteMapping("/staff/{id}") public ApiResponse<Map<String,Object>> deleteStaff(@PathVariable Long id) { return ApiResponse.success(service.deleteStaff(id)); } |
| | | @GetMapping("/merchant-settlement/onboardings") public ApiResponse<com.talentcard.server.business.PageResult<Map<String,Object>>> onboardings(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int size) { return ApiResponse.success(settlementService.onboardings(page, size)); } |
| | | @GetMapping("/merchant-settlement/bank-cards") public ApiResponse<com.talentcard.server.business.PageResult<Map<String,Object>>> bankCards(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int size) { return ApiResponse.success(settlementService.bankCards(page, size)); } |
| | | @GetMapping("/merchant-settlement/withdrawals") public ApiResponse<com.talentcard.server.business.PageResult<Map<String,Object>>> withdrawals(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int size) { return ApiResponse.success(settlementService.withdrawals(page, size)); } |
| | | @GetMapping("/merchant-settlement/onboardings") public ApiResponse<com.talentcard.server.business.PageResult<Map<String,Object>>> onboardings(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int size, @RequestParam(required = false) String merchantName, @RequestParam(required = false) String contactMobile, @RequestParam(required = false) String merchantType, @RequestParam(required = false) String status, @RequestParam(required = false) String startDate, @RequestParam(required = false) String endDate) { return ApiResponse.success(settlementService.onboardings(page, size, merchantName, contactMobile, merchantType, status, startDate, endDate)); } |
| | | @GetMapping("/merchant-settlement/bank-cards") public ApiResponse<com.talentcard.server.business.PageResult<Map<String,Object>>> bankCards(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int size, @RequestParam(required = false) String merchantName, @RequestParam(required = false) String contactMobile, @RequestParam(required = false) String merchantType, @RequestParam(required = false) String status, @RequestParam(required = false) String startDate, @RequestParam(required = false) String endDate) { return ApiResponse.success(settlementService.bankCards(page, size, merchantName, contactMobile, merchantType, status, startDate, endDate)); } |
| | | @GetMapping("/merchant-settlement/withdrawals") public ApiResponse<com.talentcard.server.business.PageResult<Map<String,Object>>> withdrawals(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int size, @RequestParam(required = false) String merchantName, @RequestParam(required = false) String contactMobile, @RequestParam(required = false) String merchantType, @RequestParam(required = false) String status, @RequestParam(required = false) String startDate, @RequestParam(required = false) String endDate) { return ApiResponse.success(settlementService.withdrawals(page, size, merchantName, contactMobile, merchantType, status, startDate, endDate)); } |
| | | @PatchMapping("/merchant-settlement/onboardings/{id}") public ApiResponse<Map<String,Object>> reviewOnboarding(@PathVariable Long id, @RequestBody Map<String,String> body) { return ApiResponse.success(settlementService.reviewOnboarding(id, body.get("status"), body.get("reason"), body.get("huifuId"))); } |
| | | @PatchMapping("/merchant-settlement/bank-cards/{id}") public ApiResponse<Map<String,Object>> reviewBankCard(@PathVariable Long id, @RequestBody Map<String,String> body) { return ApiResponse.success(settlementService.reviewBankCard(id, body.get("status"), body.get("reason"), body.get("tokenNo"))); } |
| | | @PatchMapping("/merchant-settlement/withdrawals/{id}") public ApiResponse<Map<String,Object>> reviewWithdrawal(@PathVariable Long id, @RequestBody Map<String,String> body) { return ApiResponse.success(settlementService.reviewWithdrawal(id, body.get("status"), body.get("reason"))); } |
| | |
| | | package com.talentcard.server.business.admin; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | |
| | | public MerchantSettlementAdminService(JdbcTemplate jdbc, HuifuMerchantWithdrawalService withdrawalService) { this.jdbc = jdbc; this.withdrawalService = withdrawalService; } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public PageResult<Map<String, Object>> onboardings(int page, int size) { return page("select a.*, m.merchant_name, s.store_name from merchant_onboarding_application a join merchant m on m.id = a.merchant_id join merchant_store s on s.id = a.store_id", "select count(*) from merchant_onboarding_application", page, size); } |
| | | public PageResult<Map<String, Object>> onboardings(int page, int size, String merchantName, String contactMobile, String merchantType, String status, String startDate, String endDate) { |
| | | return page("select a.*, m.merchant_name, m.merchant_type, m.contact_mobile, s.store_name from merchant_onboarding_application a join merchant m on m.id = a.merchant_id join merchant_store s on s.id = a.store_id", "select count(*) from merchant_onboarding_application a join merchant m on m.id = a.merchant_id join merchant_store s on s.id = a.store_id", "a", page, size, merchantName, contactMobile, merchantType, status, startDate, endDate); |
| | | } |
| | | @Transactional(readOnly = true) |
| | | public PageResult<Map<String, Object>> bankCards(int page, int size) { return page("select c.*, m.merchant_name, s.store_name from merchant_bank_card c join merchant m on m.id = c.merchant_id join merchant_store s on s.id = c.store_id", "select count(*) from merchant_bank_card", page, size); } |
| | | public PageResult<Map<String, Object>> bankCards(int page, int size, String merchantName, String contactMobile, String merchantType, String status, String startDate, String endDate) { |
| | | return page("select c.*, m.merchant_name, m.merchant_type, m.contact_mobile, s.store_name from merchant_bank_card c join merchant m on m.id = c.merchant_id join merchant_store s on s.id = c.store_id", "select count(*) from merchant_bank_card c join merchant m on m.id = c.merchant_id join merchant_store s on s.id = c.store_id", "c", page, size, merchantName, contactMobile, merchantType, status, startDate, endDate); |
| | | } |
| | | @Transactional(readOnly = true) |
| | | public PageResult<Map<String, Object>> withdrawals(int page, int size) { return page("select w.*, c.bank_name, c.card_masked, m.merchant_name, s.store_name from merchant_withdrawal w join merchant_bank_card c on c.id = w.bank_card_id join merchant m on m.id = w.merchant_id join merchant_store s on s.id = w.store_id", "select count(*) from merchant_withdrawal", page, size); } |
| | | public PageResult<Map<String, Object>> withdrawals(int page, int size, String merchantName, String contactMobile, String merchantType, String status, String startDate, String endDate) { |
| | | String available = "greatest(0, coalesce((select sum(o.pay_amount) from benefit_order o where o.merchant_id = w.merchant_id and (o.store_id is null or o.store_id = w.store_id) and o.order_status = 1 and o.verify_status = 1 and o.deleted = 0), 0) - coalesce((select sum(w2.amount_cents) from merchant_withdrawal w2 where w2.merchant_id = w.merchant_id and w2.store_id = w.store_id and w2.status in ('pending', 'processing', 'paid')), 0))"; |
| | | String select = "select w.*, c.bank_name, c.card_masked, m.merchant_name, m.merchant_type, m.contact_mobile, s.store_name, " + available + " as available_amount_cents from merchant_withdrawal w join merchant_bank_card c on c.id = w.bank_card_id join merchant m on m.id = w.merchant_id join merchant_store s on s.id = w.store_id"; |
| | | String count = "select count(*) from merchant_withdrawal w join merchant_bank_card c on c.id = w.bank_card_id join merchant m on m.id = w.merchant_id join merchant_store s on s.id = w.store_id"; |
| | | return page(select, count, "w", page, size, merchantName, contactMobile, merchantType, status, startDate, endDate); |
| | | } |
| | | |
| | | @Transactional public Map<String, Object> reviewOnboarding(Long id, String status, String reason, String huifuId) { return reviewOnboardingInternal(id, status, reason, huifuId); } |
| | | @Transactional public Map<String, Object> reviewBankCard(Long id, String status, String reason, String tokenNo) { return reviewBankCardInternal(id, status, reason, tokenNo); } |
| | |
| | | private String reviewStatus(String status) { String value=status==null?"":status.trim().toLowerCase(); if(!List.of("approved","rejected").contains(value))throw new BusinessException(ErrorCode.BAD_REQUEST,"Invalid review status");return value; } |
| | | private String normalizeWithdrawalStatus(String status) { String value=status==null?"":status.trim().toLowerCase(); if(!List.of("paid","failed").contains(value))throw new BusinessException(ErrorCode.BAD_REQUEST,"Invalid withdrawal status");return value; } |
| | | |
| | | private PageResult<Map<String, Object>> page(String query, String count, int page, int size) { int current = Math.max(1, page); int limit = Math.min(Math.max(1, size), 100); List<Map<String, Object>> values = rows(query + " order by id desc limit ? offset ?", limit, (current - 1) * limit); Long total = jdbc.queryForObject(count, Long.class); return new PageResult<>(values, current, limit, total == null ? 0 : total); } |
| | | private PageResult<Map<String, Object>> page(String query, String count, String tableAlias, int page, int size, String merchantName, String contactMobile, String merchantType, String status, String startDate, String endDate) { |
| | | int current = Math.max(1, page); |
| | | int limit = Math.min(Math.max(1, size), 100); |
| | | StringBuilder where = new StringBuilder(" where 1 = 1"); |
| | | List<Object> args = new ArrayList<>(); |
| | | addLike(where, args, "m.merchant_name", merchantName); |
| | | addLike(where, args, "m.contact_mobile", contactMobile); |
| | | addEqual(where, args, "m.merchant_type", merchantType); |
| | | addEqual(where, args, tableAlias + ".status", status); |
| | | addDate(where, args, tableAlias + ".created_at", startDate, true); |
| | | addDate(where, args, tableAlias + ".created_at", endDate, false); |
| | | List<Object> rowArgs = new ArrayList<>(args); |
| | | rowArgs.add(limit); |
| | | rowArgs.add((current - 1) * limit); |
| | | List<Map<String, Object>> values = rows(query + where + " order by " + tableAlias + ".id desc limit ? offset ?", rowArgs.toArray()); |
| | | Long total = jdbc.queryForObject(count + where, Long.class, args.toArray()); |
| | | return new PageResult<>(values, current, limit, total == null ? 0 : total); |
| | | } |
| | | private void addLike(StringBuilder where, List<Object> args, String column, String value) { if (value != null && !value.trim().isEmpty()) { where.append(" and ").append(column).append(" like ?"); args.add("%" + value.trim() + "%"); } } |
| | | private void addEqual(StringBuilder where, List<Object> args, String column, String value) { if (value != null && !value.trim().isEmpty()) { where.append(" and ").append(column).append(" = ?"); args.add(value.trim()); } } |
| | | private void addDate(StringBuilder where, List<Object> args, String column, String value, boolean start) { if (value != null && value.matches("\\d{4}-\\d{2}-\\d{2}")) { where.append(" and ").append(column).append(start ? " >= ?" : " < date_add(?, interval 1 day)"); args.add(value); } } |
| | | private Map<String, Object> update(String table, Long id, String status, String reason, String label) { |
| | | String value = status == null ? "" : status.trim().toLowerCase(); |
| | | if (!List.of("approved", "rejected", "paid", "failed").contains(value)) throw new BusinessException(ErrorCode.BAD_REQUEST, "审核状态无效"); |