优化结构

This commit is contained in:
Yvan
2026-08-21 08:54:29 +08:00
parent c968f9f24e
commit 77e192b6c0
167 changed files with 3657 additions and 1528 deletions
+12
View File
@@ -19,3 +19,15 @@ export const getPaymentOrder = (data) => service({
method: 'post',
data
})
export const queryPaymentOrder = (data) => service({
url: '/payment/query',
method: 'post',
data
})
export const refundPaymentOrder = (data) => service({
url: '/payment/refund',
method: 'post',
data
})
+22 -19
View File
@@ -1,12 +1,16 @@
<template>
<div class="kra-table-box">
<el-alert title="支付密钥只用于保存和调用渠道,列表接口会自动脱敏。提交 ****** 会保留原密钥。" type="warning" :closable="false" class="mb-4" />
<el-table :data="configs" row-key="provider" stripe>
<el-table-column prop="provider" label="渠道" width="160" />
<el-table-column label="状态" width="110"><template #default="scope"><el-switch v-model="scope.row.enabled" @change="save(scope.row)" /></template></el-table-column>
<el-table-column label="配置 JSON" min-width="520"><template #default="scope"><el-input v-model="scope.row.editor" type="textarea" :rows="4" spellcheck="false" /></template></el-table-column>
<el-table-column label="操作" width="110"><template #default="scope"><el-button type="primary" link icon="check" @click="save(scope.row)">保存</el-button></template></el-table-column>
</el-table>
<div class="payment-config">
<el-alert title="密钥字段会由服务端脱敏。编辑时保留 ****** 可继续使用原密钥;保存前请先校验 JSON。" type="warning" :closable="false" class="mb-4" />
<div class="kra-table-box">
<div class="config-toolbar"><span>已配置 {{ configs.length }} 个支付渠道</span><el-button :loading="loading" icon="refresh" @click="load">刷新</el-button></div>
<el-table v-loading="loading" :data="configs" row-key="provider" stripe>
<el-table-column prop="provider" label="渠道" width="170" />
<el-table-column label="启用" width="100"><template #default="scope"><el-switch v-model="scope.row.enabled" @change="save(scope.row)" /></template></el-table-column>
<el-table-column label="配置 JSON" min-width="560"><template #default="scope"><el-input v-model="scope.row.editor" type="textarea" :rows="5" spellcheck="false" /></template></el-table-column>
<el-table-column label="校验" width="100"><template #default="scope"><el-tag :type="scope.row.valid === false ? 'danger' : 'success'">{{ scope.row.valid === false ? '格式错误' : '可保存' }}</el-tag></template></el-table-column>
<el-table-column label="操作" width="130"><template #default="scope"><el-button type="primary" link icon="check" @click="save(scope.row)">保存</el-button><el-button link @click="pretty(scope.row)">格式化</el-button></template></el-table-column>
</el-table>
</div>
</div>
</template>
@@ -15,16 +19,15 @@ import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { getPaymentConfigs, savePaymentConfig } from '@/api/payment'
const configs = ref([])
const load = async () => {
const res = await getPaymentConfigs()
if (res.code === 0) configs.value = (res.data || []).map((item) => ({ ...item, editor: JSON.stringify(item.config || {}, null, 2) }))
}
const save = async (row) => {
let config
try { config = JSON.parse(row.editor || '{}') } catch { ElMessage.error('配置必须是合法 JSON'); return }
const res = await savePaymentConfig({ provider: row.provider, enabled: row.enabled, config })
if (res.code === 0) { ElMessage.success('保存成功'); await load() }
}
const configs = ref([]); const loading = ref(false)
const parseConfig = (row, notify = true) => { try { JSON.parse(row.editor || '{}'); row.valid = true; return true } catch { row.valid = false; if (notify) ElMessage.error(`${row.provider} 配置必须是合法 JSON`); return false } }
const load = async () => { loading.value = true; try { const res = await getPaymentConfigs(); if (res.code === 0) configs.value = (res.data || []).map((item) => ({ ...item, editor: JSON.stringify(item.config || {}, null, 2), valid: true })) } finally { loading.value = false } }
const pretty = (row) => { try { row.editor = JSON.stringify(JSON.parse(row.editor || '{}'), null, 2); row.valid = true } catch { parseConfig(row) } }
const save = async (row) => { if (!parseConfig(row)) return; const config = JSON.parse(row.editor || '{}'); const res = await savePaymentConfig({ provider: row.provider, enabled: row.enabled, config }); if (res.code === 0) { ElMessage.success(`${row.provider} 配置保存成功`); await load() } }
load()
</script>
<style scoped>
.config-toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; color: var(--el-text-color-secondary); }
.payment-config { min-width: 900px; }
</style>
+42 -29
View File
@@ -1,55 +1,68 @@
<template>
<div>
<div class="payment-orders">
<div class="kra-search-box">
<el-form :inline="true" :model="searchInfo">
<el-form :inline="true" :model="searchInfo" @submit.prevent="reload">
<el-form-item label="渠道"><el-input v-model="searchInfo.provider" placeholder="alipay / wechat-v3" clearable /></el-form-item>
<el-form-item label="商户订单号"><el-input v-model="searchInfo.tradeNo" clearable /></el-form-item>
<el-form-item label="业务类型"><el-input v-model="searchInfo.businessType" clearable /></el-form-item>
<el-form-item label="支付状态">
<el-select v-model="searchInfo.paymentStatus" clearable placeholder="全部">
<el-option label="待支付" value="pending" /><el-option label="已支付" value="paid" />
<el-option label="失败" value="failed" /><el-option label="已退款" value="refunded" />
</el-select>
</el-form-item>
<el-form-item label="业务 ID"><el-input v-model="searchInfo.businessId" clearable /></el-form-item>
<el-form-item label="支付状态"><el-select v-model="searchInfo.paymentStatus" clearable placeholder="全部" style="width: 130px"><el-option v-for="item in paymentStatuses" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
<el-form-item label="退款状态"><el-select v-model="searchInfo.refundStatus" clearable placeholder="全部" style="width: 130px"><el-option v-for="item in refundStatuses" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
<el-form-item><el-button type="primary" icon="search" @click="reload">查询</el-button><el-button icon="refresh" @click="reset">重置</el-button></el-form-item>
</el-form>
</div>
<div class="kra-table-box">
<el-table :data="rows" row-key="ID" stripe>
<el-table v-loading="loading" :data="rows" row-key="ID" stripe>
<el-table-column prop="tradeNo" label="商户订单号" min-width="190" show-overflow-tooltip />
<el-table-column prop="provider" label="渠道" width="120" />
<el-table-column prop="subject" label="商品/标题" min-width="180" show-overflow-tooltip />
<el-table-column label="金额" width="130"><template #default="scope">{{ scope.row.amount }} {{ scope.row.currency }}</template></el-table-column>
<el-table-column label="金额" width="130"><template #default="scope">{{ formatAmount(scope.row.amount, scope.row.currency) }}</template></el-table-column>
<el-table-column label="支付状态" width="110"><template #default="scope"><el-tag :type="statusType(scope.row.paymentStatus)">{{ statusText(scope.row.paymentStatus) }}</el-tag></template></el-table-column>
<el-table-column label="发货" width="110"><template #default="scope"><el-tag :type="scope.row.fulfillmentStatus === 'succeeded' ? 'success' : 'info'">{{ scope.row.fulfillmentStatus }}</el-tag></template></el-table-column>
<el-table-column label="退款" width="110"><template #default="scope">{{ scope.row.refundStatus }}</template></el-table-column>
<el-table-column prop="businessId" label="业务 ID" min-width="150" show-overflow-tooltip />
<el-table-column label="发货" width="110"><template #default="scope"><el-tag :type="fulfillmentType(scope.row.fulfillmentStatus)">{{ fulfillmentText(scope.row.fulfillmentStatus) }}</el-tag></template></el-table-column>
<el-table-column label="退款" width="110"><template #default="scope"><el-tag :type="refundType(scope.row.refundStatus)">{{ refundText(scope.row.refundStatus) }}</el-tag></template></el-table-column>
<el-table-column label="创建时间" width="180"><template #default="scope">{{ formatDate(scope.row.createdAt) }}</template></el-table-column>
<el-table-column label="操作" fixed="right" width="210"><template #default="scope"><el-button link type="primary" @click="openDetail(scope.row)">详情</el-button><el-button link type="primary" @click="refreshOrder(scope.row)">同步</el-button><el-button v-if="canRefund(scope.row)" link type="warning" @click="openRefund(scope.row)">退款</el-button><el-button v-if="canFulfill(scope.row)" link type="success" @click="showFulfillmentHint">发货</el-button></template></el-table-column>
</el-table>
<div class="kra-pagination"><el-pagination :current-page="page" :page-size="pageSize" :page-sizes="[10, 30, 50, 100]" :total="total" layout="total, sizes, prev, pager, next, jumper" @current-change="changePage" @size-change="changeSize" /></div>
</div>
<el-drawer v-model="detailVisible" title="订单详情" size="560px" destroy-on-close>
<el-descriptions v-if="detail" :column="2" border>
<el-descriptions-item label="商户订单号" :span="2">{{ detail.tradeNo || '-' }}</el-descriptions-item><el-descriptions-item label="渠道">{{ detail.provider || '-' }}</el-descriptions-item><el-descriptions-item label="支付方式">{{ detail.paymentMode || '-' }}</el-descriptions-item><el-descriptions-item label="业务类型">{{ detail.businessType || '-' }}</el-descriptions-item><el-descriptions-item label="业务 ID">{{ detail.businessId || '-' }}</el-descriptions-item><el-descriptions-item label="商品标题" :span="2">{{ detail.subject || '-' }}</el-descriptions-item><el-descriptions-item label="订单金额">{{ formatAmount(detail.amount, detail.currency) }}</el-descriptions-item><el-descriptions-item label="实付金额">{{ formatAmount(detail.paidAmount, detail.currency) }}</el-descriptions-item><el-descriptions-item label="支付状态"><el-tag :type="statusType(detail.paymentStatus)">{{ statusText(detail.paymentStatus) }}</el-tag></el-descriptions-item><el-descriptions-item label="发货状态"><el-tag :type="fulfillmentType(detail.fulfillmentStatus)">{{ fulfillmentText(detail.fulfillmentStatus) }}</el-tag></el-descriptions-item><el-descriptions-item label="退款状态"><el-tag :type="refundType(detail.refundStatus)">{{ refundText(detail.refundStatus) }}</el-tag></el-descriptions-item><el-descriptions-item label="已退款金额">{{ formatAmount(detail.refundedAmount, detail.currency) }}</el-descriptions-item><el-descriptions-item label="第三方订单号" :span="2">{{ detail.providerTradeNo || '-' }}</el-descriptions-item><el-descriptions-item label="创建时间">{{ formatDate(detail.createdAt) }}</el-descriptions-item><el-descriptions-item label="支付时间">{{ formatDate(detail.paidAt) }}</el-descriptions-item><el-descriptions-item v-if="detail.lastError" label="最近错误" :span="2"><span class="error-text">{{ detail.lastError }}</span></el-descriptions-item>
</el-descriptions>
</el-drawer>
<el-dialog v-model="refundVisible" title="申请退款" width="440px" destroy-on-close>
<el-form ref="refundFormRef" :model="refundForm" :rules="refundRules" label-width="100px"><el-form-item label="订单号"><el-input :model-value="refundForm.tradeNo" disabled /></el-form-item><el-form-item label="可退金额"><span>{{ formatAmount(refundForm.maxAmount, refundForm.currency) }}</span></el-form-item><el-form-item label="退款金额" prop="amount"><el-input-number v-model="refundForm.amount" :min="1" :max="refundForm.maxAmount" :step="1" controls-position="right" style="width: 100%" /><div class="form-tip">单位为最小货币单位(例如人民币分)</div></el-form-item></el-form>
<template #footer><el-button @click="refundVisible = false">取消</el-button><el-button type="warning" :loading="refundLoading" @click="submitRefund">确认退款</el-button></template>
</el-dialog>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { getPaymentOrders } from '@/api/payment'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getPaymentOrders, getPaymentOrder, queryPaymentOrder, refundPaymentOrder } from '@/api/payment'
import { formatDate } from '@/utils/format'
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
const rows = ref([])
const searchInfo = ref({})
const statusText = (status) => ({ initialized: '初始化', pending: '待支付', paid: '已支付', failed: '失败', closed: '已关闭', partially_refunded: '部分退款', refunded: '已退款' }[status] || status || '-')
const statusType = (status) => ({ paid: 'success', refunded: 'success', failed: 'danger', closed: 'info' }[status] || 'warning')
const load = async () => {
const res = await getPaymentOrders({ page: page.value, pageSize: pageSize.value, ...searchInfo.value })
if (res.code === 0) { rows.value = res.data.list || []; total.value = res.data.total || 0; page.value = res.data.page || page.value; pageSize.value = res.data.pageSize || pageSize.value }
}
const reload = () => { page.value = 1; load() }
const reset = () => { searchInfo.value = {}; reload() }
const changePage = (value) => { page.value = value; load() }
const changeSize = (value) => { pageSize.value = value; page.value = 1; load() }
const page = ref(1); const pageSize = ref(10); const total = ref(0); const rows = ref([]); const loading = ref(false); const searchInfo = ref({}); const detail = ref(null); const detailVisible = ref(false); const refundVisible = ref(false); const refundLoading = ref(false); const refundFormRef = ref(); const refundForm = ref({ amount: 0, maxAmount: 0 })
const paymentStatuses = [{ value: 'initialized', label: '初始化' }, { value: 'pending', label: '待支付' }, { value: 'paid', label: '已支付' }, { value: 'failed', label: '失败' }, { value: 'closed', label: '已关闭' }]
const refundStatuses = [{ value: 'none', label: '未退款' }, { value: 'processing', label: '退款中' }, { value: 'partially_refunded', label: '部分退款' }, { value: 'refunded', label: '已退款' }, { value: 'failed', label: '退款失败' }]
const refundRules = { amount: [{ required: true, message: '请输入退款金额', trigger: 'blur' }, { validator: (_rule, value, callback) => value > 0 && value <= refundForm.value.maxAmount ? callback() : callback(new Error('退款金额超出可退范围')), trigger: 'change' }] }
const formatAmount = (value, currency = 'CNY') => `${((Number(value) || 0) / 100).toFixed(2)} ${currency || ''}`.trim(); const statusText = (s) => ({ initialized: '初始化', pending: '待支付', paid: '已支付', failed: '失败', closed: '已关闭' }[s] || s || '-'); const statusType = (s) => ({ paid: 'success', failed: 'danger', closed: 'info' }[s] || 'warning'); const fulfillmentText = (s) => ({ pending: '待发货', processing: '发货中', succeeded: '已发货', failed: '发货失败' }[s] || s || '-'); const fulfillmentType = (s) => ({ succeeded: 'success', failed: 'danger', processing: 'warning' }[s] || 'info'); const refundText = (s) => ({ none: '未退款', processing: '退款中', partially_refunded: '部分退款', refunded: '已退款', failed: '退款失败' }[s] || s || '-'); const refundType = (s) => ({ refunded: 'success', failed: 'danger', processing: 'warning', partially_refunded: 'warning' }[s] || 'info')
const canRefund = (r) => r.paymentStatus === 'paid' && !['refunded', 'processing'].includes(r.refundStatus) && Number(r.amount || 0) > Number(r.refundedAmount || 0); const canFulfill = (r) => r.paymentStatus === 'paid' && r.fulfillmentStatus !== 'succeeded'
const load = async () => { loading.value = true; try { const res = await getPaymentOrders({ page: page.value, pageSize: pageSize.value, ...searchInfo.value }); if (res.code === 0) { rows.value = res.data?.list || []; total.value = res.data?.total || 0; page.value = res.data?.page || page.value; pageSize.value = res.data?.pageSize || pageSize.value } } finally { loading.value = false } }
const reload = () => { page.value = 1; load() }; const reset = () => { searchInfo.value = {}; reload() }; const changePage = (v) => { page.value = v; load() }; const changeSize = (v) => { pageSize.value = v; page.value = 1; load() }
const openDetail = async (row) => { detail.value = row; detailVisible.value = true; const res = await getPaymentOrder({ provider: row.provider, tradeNo: row.tradeNo }); if (res.code === 0 && res.data) detail.value = res.data }
const refreshOrder = async (row) => { const res = await queryPaymentOrder({ provider: row.provider, tradeNo: row.tradeNo }); if (res.code === 0) { ElMessage.success('已同步支付状态'); await load() } }
const openRefund = (row) => { const maxAmount = Number(row.amount || 0) - Number(row.refundedAmount || 0); refundForm.value = { provider: row.provider, tradeNo: row.tradeNo, currency: row.currency, maxAmount, amount: maxAmount }; refundVisible.value = true }
const submitRefund = async () => { await refundFormRef.value?.validate(); await ElMessageBox.confirm('退款操作将调用支付渠道,确认继续吗?', '确认退款', { type: 'warning' }); refundLoading.value = true; try { const res = await refundPaymentOrder({ provider: refundForm.value.provider, tradeNo: refundForm.value.tradeNo, amount: refundForm.value.amount }); if (res.code === 0) { ElMessage.success('退款请求已提交'); refundVisible.value = false; await load() } } finally { refundLoading.value = false } }
const showFulfillmentHint = () => ElMessage.info('发货接口已预留,待业务模块注册发货处理器后启用。')
load()
</script>
<style scoped>
.payment-orders { min-width: 980px; }
.error-text { color: var(--el-color-danger); word-break: break-word; }
.form-tip { color: var(--el-text-color-secondary); font-size: 12px; line-height: 1.4; margin-top: 4px; }
</style>