0.使用模拟数据进行充值和提现

1.优化提现接口/api/finance/withdrawCreate
2.优化充值接口/api/finance/depositCreate
This commit is contained in:
2026-05-20 15:57:19 +08:00
parent b9e4d806f7
commit 1b8d947f97
25 changed files with 2022 additions and 179 deletions

View File

@@ -12,7 +12,7 @@ export default {
'status 0': 'Pending',
'status 1': 'Success',
'status 2': 'Failed',
'status 3': 'Canceled',
'status 3': 'Pending review',
pay_channel: 'Pay channel',
pay_time: 'Pay time',
deposit_tier_id: 'Deposit tier',
@@ -23,4 +23,18 @@ export default {
channel_name: 'Channel',
detail_title: 'Deposit Order Detail',
close_btn: 'Close',
reject_reason: 'Reject reason',
review_admin_username: 'Reviewer',
review_time: 'Review time',
review_title: 'Deposit review',
review_reject_title: 'Reject deposit',
review_btn: 'Review',
review_btn_approve: 'Approve',
review_btn_reject: 'Reject',
review_btn_back: 'Back',
review_btn_confirm_reject: 'Confirm reject',
review_reject_tip: 'After rejection, the order is marked failed and no funds will be credited.',
review_reject_placeholder: 'Enter reject reason',
reject_reason_required: 'Please enter reject reason',
already_reviewed: 'This order has already been reviewed',
}

View File

@@ -28,6 +28,7 @@ export default {
channel_name: 'Channel',
review_admin_username: 'Reviewer',
review_title: 'Withdraw review',
review_btn: 'Review',
review_reject_title: 'Reject withdraw',
review_btn_approve: 'Approve',
review_btn_reject: 'Reject',

View File

@@ -39,7 +39,7 @@ export default {
ddpay_spec_intro:
'当前提现走 DDPay 出金Payout移动端调用 withdrawCreate充值渠道为 ddpay 时调用 depositCreate。下列为字段约定摘要详细以仓库内 DDPay 文档与《36字花-移动端接口设计草案》为准。',
ddpay_spec_li_withdraw:
'提现必填channel_code=ddpay(支付渠道、withdraw_coin、receive_type=bank、receive_account(收款账号)、receiver_name(与银行登记一致)、receiver_email、receiver_mobile、bank_code(须与本页「提现支持银行(按币种)」中 code 一致)、idempotency_keybank_branch 选填,不传则服务端按 N/A 提交。',
'充值联调可用 channel_code=mock模拟支付无需 DDPay 配置);生产请用 ddpay。提现必填channel_code=ddpay 或 mock模拟、withdraw_coin、receive_type=bank、receive_account、receiver_name、receiver_email、receiver_mobile、bank_code、idempotency_keybank_branch 选填。',
ddpay_spec_li_bank_table:
'「银行名(英文)」将映射为 DDPay 的 bank[name],请与 DDPay 官方银行全称列表一致,否则出金可能被拒。',
ddpay_spec_li_deposit:

View File

@@ -14,7 +14,7 @@ export default {
'status 0': '待支付',
'status 1': '成功',
'status 2': '失败',
'status 3': '已取消',
'status 3': '待审核',
pay_channel: '支付通道',
pay_time: '支付时间',
deposit_tier_id: '充值档位',
@@ -23,4 +23,18 @@ export default {
update_time: '更新时间',
detail_title: '充值订单详情',
close_btn: '关闭',
reject_reason: '驳回原因',
review_admin_username: '审核人',
review_time: '审核时间',
review_title: '充值审核',
review_reject_title: '拒绝充值',
review_btn: '审核',
review_btn_approve: '通过',
review_btn_reject: '拒绝',
review_btn_back: '返回',
review_btn_confirm_reject: '确认拒绝',
review_reject_tip: '拒绝后订单将标记为失败,资金不会入账。',
review_reject_placeholder: '请输入驳回原因',
reject_reason_required: '请输入驳回原因',
already_reviewed: '该订单已审核,无需重复操作',
}

View File

@@ -28,6 +28,7 @@ export default {
channel_name: '渠道',
review_admin_username: '审核人',
review_title: '提现审核',
review_btn: '审核',
review_reject_title: '提现拒绝',
review_btn_approve: '通过',
review_btn_reject: '拒绝',

View File

@@ -30,6 +30,22 @@ defineOptions({
const { t } = useI18n()
const tableRef = useTemplateRef('tableRef')
const optButtons: OptButton[] = defaultOptButtons(['edit'])
const depositEditBtn = optButtons.find((b) => b.name === 'edit')
if (depositEditBtn) {
depositEditBtn.display = (row: TableRow) => Number(row.status) !== 3
}
optButtons.push({
render: 'tipButton',
name: 'depositReview',
title: 'order.depositOrder.review_btn',
text: '',
type: 'warning',
icon: 'fa fa-check-square-o',
display: (row: TableRow) => Number(row.status) === 3,
click: (row: TableRow) => {
baTable.toggleForm('Edit', [row[baTable.table.pk!]])
},
})
function formatAmount(_row: anyObj, _column: any, cellValue: unknown) {
if (cellValue === null || cellValue === undefined || cellValue === '') {
@@ -186,7 +202,7 @@ const baTable = new baTableClass(
{
label: t('Operate'),
align: 'center',
width: 90,
width: 120,
render: 'buttons',
buttons: optButtons,
operator: false,

View File

@@ -1,6 +1,6 @@
<template>
<el-dialog
class="ba-operate-dialog deposit-detail-dialog"
class="ba-operate-dialog deposit-review-dialog"
:close-on-click-modal="false"
:model-value="isOpen"
width="640px"
@@ -8,7 +8,13 @@
>
<template #header>
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
{{ t('order.depositOrder.detail_title') }}
{{
step === 'reject'
? t('order.depositOrder.review_reject_title')
: isPendingReview
? t('order.depositOrder.review_title')
: t('order.depositOrder.detail_title')
}}
</div>
</template>
@@ -18,7 +24,7 @@
:style="config.layout.shrink ? '' : 'width: calc(100% - ' + (baTable.form.labelWidth ?? 120) / 2 + 'px)'"
>
<el-form
v-if="!loading"
v-if="!loading && step === 'review'"
:label-position="config.layout.shrink ? 'top' : 'right'"
:label-width="(baTable.form.labelWidth ?? 120) + 'px'"
@submit.prevent=""
@@ -38,7 +44,6 @@
<el-form-item :label="t('order.depositOrder.status')">
<el-tag :type="statusTagType" effect="dark" size="small">{{ statusLabel }}</el-tag>
</el-form-item>
<el-form-item :label="t('order.depositOrder.amount')">
<el-input :model-value="amountText" readonly />
</el-form-item>
@@ -48,7 +53,6 @@
<el-form-item :label="t('order.depositOrder.total_credit')">
<el-input :model-value="totalCreditText" readonly />
</el-form-item>
<el-form-item :label="t('order.depositOrder.pay_channel')">
<el-input :model-value="form.pay_channel || '-'" readonly />
</el-form-item>
@@ -58,6 +62,15 @@
<el-form-item :label="t('order.depositOrder.deposit_tier_id')">
<el-input :model-value="form.deposit_tier_id || '-'" readonly />
</el-form-item>
<el-form-item v-if="form.reject_reason" :label="t('order.depositOrder.reject_reason')">
<el-input :model-value="form.reject_reason" type="textarea" :rows="2" readonly />
</el-form-item>
<el-form-item v-if="!isPendingReview" :label="t('order.depositOrder.review_admin_username')">
<el-input :model-value="form.review_admin_text" readonly />
</el-form-item>
<el-form-item v-if="!isPendingReview" :label="t('order.depositOrder.review_time')">
<el-input :model-value="form.review_time_text" readonly />
</el-form-item>
<el-form-item :label="t('order.depositOrder.remark')">
<el-input v-model="form.remark" type="textarea" :rows="2" readonly />
</el-form-item>
@@ -65,20 +78,67 @@
<el-input :model-value="form.create_time_text" readonly />
</el-form-item>
</el-form>
<el-form
v-if="!loading && step === 'reject'"
ref="rejectFormRef"
:model="rejectForm"
:rules="rejectRules"
:label-position="config.layout.shrink ? 'top' : 'right'"
:label-width="(baTable.form.labelWidth ?? 120) + 'px'"
@submit.prevent=""
>
<el-alert
class="review-reject-hint"
type="warning"
show-icon
:closable="false"
:title="t('order.depositOrder.review_reject_tip')"
/>
<el-form-item :label="t('order.depositOrder.order_no')">
<el-input :model-value="form.order_no" readonly />
</el-form-item>
<el-form-item :label="t('order.depositOrder.total_credit')">
<el-input :model-value="totalCreditText" readonly />
</el-form-item>
<el-form-item :label="t('order.depositOrder.reject_reason')" prop="remark">
<el-input
v-model="rejectForm.remark"
type="textarea"
:rows="4"
maxlength="255"
show-word-limit
:placeholder="t('order.depositOrder.review_reject_placeholder')"
/>
</el-form-item>
</el-form>
</div>
</el-scrollbar>
<template #footer>
<div class="detail-footer">
<el-button type="primary" v-blur @click="onDialogClose">{{ t('order.depositOrder.close_btn') }}</el-button>
<div class="review-footer">
<template v-if="step === 'review'">
<el-button @click="onDialogClose">{{ isPendingReview ? t('Cancel') : t('order.depositOrder.close_btn') }}</el-button>
<template v-if="isPendingReview">
<el-button type="danger" :loading="submitting" @click="gotoReject">{{ t('order.depositOrder.review_btn_reject') }}</el-button>
<el-button type="primary" v-blur :loading="submitting" @click="submitApprove">{{ t('order.depositOrder.review_btn_approve') }}</el-button>
</template>
</template>
<template v-else>
<el-button @click="backToReview">{{ t('order.depositOrder.review_btn_back') }}</el-button>
<el-button type="danger" v-blur :loading="submitting" @click="submitReject">{{ t('order.depositOrder.review_btn_confirm_reject') }}</el-button>
</template>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, inject, reactive, ref, watch } from 'vue'
import type { FormInstance, FormItemRule } from 'element-plus'
import { ElMessage } from 'element-plus'
import { computed, inject, reactive, ref, useTemplateRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import createAxios from '/@/utils/axios'
import { useConfig } from '/@/stores/config'
import type baTableClass from '/@/utils/baTable'
@@ -86,7 +146,12 @@ const config = useConfig()
const baTable = inject('baTable') as baTableClass
const { t } = useI18n()
const rejectFormRef = useTemplateRef<FormInstance>('rejectFormRef')
type Step = 'review' | 'reject'
const step = ref<Step>('review')
const loading = ref(false)
const submitting = ref(false)
const form = reactive({
id: 0,
@@ -98,13 +163,29 @@ const form = reactive({
pay_time_text: '-',
deposit_tier_id: '',
remark: '',
reject_reason: '',
create_time_text: '-',
review_time_text: '-',
review_admin_text: '-',
amount: 0,
bonus_amount: 0,
status: 0,
})
const rejectForm = reactive({
remark: '',
})
const isOpen = computed(() => ['Edit'].includes(baTable.form.operate ?? ''))
const isPendingReview = computed(() => form.status === 3)
watch(isOpen, (visible) => {
if (!visible) {
return
}
step.value = 'review'
rejectForm.remark = ''
})
watch(
() => ({ visible: isOpen.value, loadingState: baTable.form.loading, items: baTable.form.items }),
@@ -128,13 +209,16 @@ const hydrate = () => {
form.pay_channel = String(row['pay_channel'] ?? '')
form.deposit_tier_id = String(row['deposit_tier_id'] ?? '')
form.remark = String(row['remark'] ?? '')
form.reject_reason = String(row['reject_reason'] ?? '')
form.amount = parseNumber(row['amount'])
form.bonus_amount = parseNumber(row['bonus_amount'])
form.status = Number(row['status'] ?? 0)
form.create_time_text = formatTime(row['create_time'])
form.pay_time_text = formatTime(row['pay_time'])
form.review_time_text = formatTime(row['review_time'])
form.user_text = resolveRelationText(row, 'user', row['user_id'])
form.channel_text = resolveRelationText(row, 'channel', row['channel_id'])
form.review_admin_text = resolveRelationText(row, 'reviewAdmin', row['review_admin_id'])
}
const statusLabel = computed(() => t('order.depositOrder.status ' + form.status))
@@ -155,10 +239,91 @@ const amountText = computed(() => formatAmount(form.amount))
const bonusText = computed(() => formatAmount(form.bonus_amount))
const totalCreditText = computed(() => formatAmount(Number((form.amount + form.bonus_amount).toFixed(2))))
const rejectRules: Record<string, FormItemRule[]> = {
remark: [
{
required: true,
validator: (_r, value, cb) => {
const text = typeof value === 'string' ? value.trim() : ''
if (text === '') {
cb(new Error(t('order.depositOrder.reject_reason_required')))
return
}
cb()
},
trigger: 'blur',
},
],
}
const onDialogClose = () => {
if (submitting.value) {
return
}
step.value = 'review'
baTable.toggleForm()
}
const gotoReject = () => {
step.value = 'reject'
rejectForm.remark = ''
}
const backToReview = () => {
step.value = 'review'
}
const submitApprove = async () => {
if (!isPendingReview.value) {
ElMessage.warning(t('order.depositOrder.already_reviewed'))
return
}
submitting.value = true
try {
await createAxios(
{
url: '/admin/order.DepositOrder/approve',
method: 'POST',
data: { id: form.id },
},
{ showSuccessMessage: true }
)
baTable.onTableHeaderAction('refresh', {})
baTable.toggleForm()
} catch (_e) {
// axios interceptor
} finally {
submitting.value = false
}
}
const submitReject = async () => {
const formEl = rejectFormRef.value
if (!formEl) return
const valid = await formEl.validate().catch(() => false)
if (!valid) return
submitting.value = true
try {
await createAxios(
{
url: '/admin/order.DepositOrder/reject',
method: 'POST',
data: {
id: form.id,
remark: rejectForm.remark.trim(),
},
},
{ showSuccessMessage: true }
)
baTable.onTableHeaderAction('refresh', {})
baTable.toggleForm()
} catch (_e) {
// axios interceptor
} finally {
submitting.value = false
}
}
function parseNumber(raw: unknown): number {
if (raw === null || raw === undefined || raw === '') return 0
const n = Number(raw)
@@ -210,22 +375,19 @@ function resolveRelationText(row: Record<string, unknown>, relationKey: string,
</script>
<style scoped lang="scss">
.deposit-detail-dialog {
.deposit-review-dialog {
:deep(.el-dialog__body) {
padding-top: 8px;
}
}
.detail-footer {
.review-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
}
@media (max-width: 640px) {
:deep(.deposit-detail-dialog) {
width: calc(100vw - 24px) !important;
max-width: 100vw;
}
.review-reject-hint {
margin-bottom: 12px;
}
</style>

View File

@@ -30,6 +30,22 @@ defineOptions({
const { t } = useI18n()
const tableRef = useTemplateRef('tableRef')
const optButtons: OptButton[] = defaultOptButtons(['edit'])
const editBtn = optButtons.find((b) => b.name === 'edit')
if (editBtn) {
editBtn.display = (row: TableRow) => Number(row.status) !== 0
}
optButtons.push({
render: 'tipButton',
name: 'review',
title: 'order.withdrawOrder.review_btn',
text: '',
type: 'warning',
icon: 'fa fa-check-square-o',
display: (row: TableRow) => Number(row.status) === 0,
click: (row: TableRow) => {
baTable.toggleForm('Edit', [row[baTable.table.pk!]])
},
})
function formatAmount(_row: anyObj, _column: any, cellValue: unknown) {
if (cellValue === null || cellValue === undefined || cellValue === '') {
@@ -151,9 +167,9 @@ const baTable = new baTableClass(
effect: 'dark',
custom: {
'0': 'info',
'1': 'warning',
'2': 'success',
'3': 'danger',
'1': 'success',
'2': 'danger',
'3': 'success',
},
replaceValue: {
'0': t('order.withdrawOrder.status 0'),
@@ -213,7 +229,7 @@ const baTable = new baTableClass(
width: 170,
timeFormat: 'yyyy-mm-dd hh:MM:ss',
},
{ label: t('Operate'), align: 'center', width: 90, render: 'buttons', buttons: optButtons, operator: false, fixed: 'right' },
{ label: t('Operate'), align: 'center', width: 120, render: 'buttons', buttons: optButtons, operator: false, fixed: 'right' },
],
},
{