feat: adapt admin payment configuration and refunds
This commit is contained in:
@@ -41,3 +41,12 @@ export const retryOrderHook = (data) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**订单退款 */
|
||||||
|
export const refundOrder = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/order/refund', data, {
|
||||||
|
headers:{
|
||||||
|
'Content-Type':'multipart/form-data'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { http2 } from '@/utils/request.js'
|
||||||
|
|
||||||
|
/** 获取 007pay 支付配置 */
|
||||||
|
export const getPay007Config = () => {
|
||||||
|
return http2.get('/api/v1/admin/pay007/config')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新 007pay 支付配置 */
|
||||||
|
export const updatePay007Config = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/pay007/config/update', data, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
+5
-1
@@ -68,6 +68,10 @@ export const menus = [
|
|||||||
{
|
{
|
||||||
path: '/order/list',
|
path: '/order/list',
|
||||||
title: '订单列表'
|
title: '订单列表'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/order/payment-config',
|
||||||
|
title: '支付配置'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -227,4 +231,4 @@ export const menus = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+9
-1
@@ -313,6 +313,14 @@ const routes = [
|
|||||||
meta: {
|
meta: {
|
||||||
title: '订单列表'
|
title: '订单列表'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'payment-config',
|
||||||
|
name: 'PaymentConfig',
|
||||||
|
component: () => import('../views/order/PaymentConfig.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '支付配置'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -806,4 +814,4 @@ router.afterEach(() => {
|
|||||||
window.scrollTo(0, 0)
|
window.scrollTo(0, 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@@ -130,7 +130,15 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="支付方式" width="100">
|
<el-table-column label="支付方式" width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span>{{ row.payType || '-' }}</span>
|
<span>{{ getPayTypeText(row.payType) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="退款状态" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.refundStatus" :type="getRefundStatusType(row.refundStatus)" size="small">
|
||||||
|
{{ getRefundStatusText(row.refundStatus) }}
|
||||||
|
</el-tag>
|
||||||
|
<span v-else class="text-muted">-</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="过期时间" width="170">
|
<el-table-column label="过期时间" width="170">
|
||||||
@@ -143,11 +151,12 @@
|
|||||||
<span>{{ formatDate(row.CreatedAt) }}</span>
|
<span>{{ formatDate(row.CreatedAt) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="250" fixed="right">
|
<el-table-column label="操作" width="310" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<el-button type="primary" link @click="handleView(row)">查看</el-button>
|
<el-button type="primary" link @click="handleView(row)">查看</el-button>
|
||||||
<el-button type="warning" link @click="handleEdit(row)">编辑</el-button>
|
<el-button type="warning" link @click="handleEdit(row)">编辑</el-button>
|
||||||
|
<el-button type="success" link :disabled="!canRefund(row)" @click="handleOpenRefund(row)">退款</el-button>
|
||||||
<el-button type="danger" link @click="handleRetryOrder(row)">重试流程</el-button>
|
<el-button type="danger" link @click="handleRetryOrder(row)">重试流程</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -187,6 +196,9 @@
|
|||||||
{{ getStatusText(orderDetail.state) }}
|
{{ getStatusText(orderDetail.state) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<el-tag type="info" size="small" effect="plain">{{ getTypeText(orderDetail.type) }}</el-tag>
|
<el-tag type="info" size="small" effect="plain">{{ getTypeText(orderDetail.type) }}</el-tag>
|
||||||
|
<el-tag v-if="orderDetail.refundStatus" :type="getRefundStatusType(orderDetail.refundStatus)" size="small" effect="plain">
|
||||||
|
{{ getRefundStatusText(orderDetail.refundStatus) }}
|
||||||
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="od-hero-sub">
|
<div class="od-hero-sub">
|
||||||
<span>订单号 #{{ orderDetail.id }}</span>
|
<span>订单号 #{{ orderDetail.id }}</span>
|
||||||
@@ -202,7 +214,16 @@
|
|||||||
|
|
||||||
<!-- 订单信息 -->
|
<!-- 订单信息 -->
|
||||||
<div class="od-section">
|
<div class="od-section">
|
||||||
<div class="od-section-title"><el-icon><Document /></el-icon><span>订单信息</span></div>
|
<div class="od-section-title">
|
||||||
|
<el-icon><Document /></el-icon><span>订单信息</span>
|
||||||
|
<el-button
|
||||||
|
v-if="canRefund(orderDetail)"
|
||||||
|
type="success"
|
||||||
|
link
|
||||||
|
class="od-section-action"
|
||||||
|
@click="handleOpenRefund(orderDetail)"
|
||||||
|
>发起退款</el-button>
|
||||||
|
</div>
|
||||||
<el-descriptions :column="2" border size="small">
|
<el-descriptions :column="2" border size="small">
|
||||||
<el-descriptions-item label="订单ID">{{ orderDetail.id }}</el-descriptions-item>
|
<el-descriptions-item label="订单ID">{{ orderDetail.id }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="订单类型">
|
<el-descriptions-item label="订单类型">
|
||||||
@@ -217,7 +238,12 @@
|
|||||||
<span class="od-mono">{{ orderDetail.paymentOrderId || '-' }}</span>
|
<span class="od-mono">{{ orderDetail.paymentOrderId || '-' }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="到期时间">{{ formatDate(orderDetail.expireTime) }}</el-descriptions-item>
|
<el-descriptions-item label="到期时间">{{ formatDate(orderDetail.expireTime) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="退款时间">{{ orderDetail.refundTime ? formatDate(orderDetail.refundTime) : '-' }}</el-descriptions-item>
|
<el-descriptions-item label="退款状态">
|
||||||
|
<el-tag v-if="orderDetail.refundStatus" :type="getRefundStatusType(orderDetail.refundStatus)" size="small">
|
||||||
|
{{ getRefundStatusText(orderDetail.refundStatus) }}
|
||||||
|
</el-tag>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="创建时间">{{ formatDate(orderDetail.createdAt || orderDetail.CreatedAt) }}</el-descriptions-item>
|
<el-descriptions-item label="创建时间">{{ formatDate(orderDetail.createdAt || orderDetail.CreatedAt) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="更新时间">{{ formatDate(orderDetail.updatedAt || orderDetail.UpdatedAt) }}</el-descriptions-item>
|
<el-descriptions-item label="更新时间">{{ formatDate(orderDetail.updatedAt || orderDetail.UpdatedAt) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="参数信息" :span="2">
|
<el-descriptions-item label="参数信息" :span="2">
|
||||||
@@ -231,6 +257,26 @@
|
|||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 退款信息 -->
|
||||||
|
<div class="od-section" v-if="hasRefundInfo(orderDetail)">
|
||||||
|
<div class="od-section-title">
|
||||||
|
<el-icon><Money /></el-icon><span>退款信息</span>
|
||||||
|
</div>
|
||||||
|
<el-descriptions :column="2" border size="small">
|
||||||
|
<el-descriptions-item label="退款状态">
|
||||||
|
<el-tag :type="getRefundStatusType(orderDetail.refundStatus)" size="small">
|
||||||
|
{{ getRefundStatusText(orderDetail.refundStatus) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="退款金额">¥{{ formatYuan(orderDetail.refundAmount) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="退款单号">
|
||||||
|
<span class="od-mono">{{ orderDetail.refundNo || '-' }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="退款时间">{{ orderDetail.refundTime ? formatDate(orderDetail.refundTime) : '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="退款原因" :span="2">{{ orderDetail.refundReason || '-' }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 用户信息 -->
|
<!-- 用户信息 -->
|
||||||
<div class="od-section">
|
<div class="od-section">
|
||||||
<div class="od-section-title">
|
<div class="od-section-title">
|
||||||
@@ -514,9 +560,12 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="支付方式">
|
<el-form-item label="支付方式">
|
||||||
<el-select v-model="orderForm.pay_type" placeholder="请选择" clearable style="width: 100%">
|
<el-select v-model="orderForm.pay_type" placeholder="请选择" clearable style="width: 100%">
|
||||||
<el-option label="默认(余额)" value="default" />
|
<el-option label="默认/余额" value="default" />
|
||||||
<el-option label="支付宝" value="ali" />
|
<el-option label="余额" value="balance" />
|
||||||
<el-option label="微信" value="wx" />
|
<el-option label="007pay" value="007pay" />
|
||||||
|
<el-option label="管理员" value="admin" />
|
||||||
|
<el-option label="支付宝(旧)" value="ali" />
|
||||||
|
<el-option label="微信(旧)" value="wx" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
@@ -568,6 +617,79 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 退款对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="refundDialogVisible"
|
||||||
|
title="订单退款"
|
||||||
|
width="560px"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<div v-if="refundOrderInfo" class="refund-dialog-body">
|
||||||
|
<div class="refund-summary">
|
||||||
|
<div class="refund-summary-main">
|
||||||
|
<div class="refund-order-name">{{ refundOrderInfo.name || '未命名订单' }}</div>
|
||||||
|
<div class="refund-order-meta">
|
||||||
|
<span>订单 #{{ refundOrderInfo.id }}</span>
|
||||||
|
<el-divider direction="vertical" />
|
||||||
|
<span>{{ getPayTypeText(refundOrderInfo.payType) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="refund-order-amount">¥{{ formatYuan(refundOrderInfo.price) }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form ref="refundFormRef" :model="refundForm" :rules="refundRules" label-width="110px">
|
||||||
|
<el-form-item label="退款方式" prop="refund_method">
|
||||||
|
<el-radio-group v-model="refundForm.refund_method">
|
||||||
|
<el-radio-button value="original" :disabled="!canOriginalRefund(refundOrderInfo)">原路退款</el-radio-button>
|
||||||
|
<el-radio-button value="balance" :disabled="!canBalanceRefund(refundOrderInfo)">退到余额</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="退款金额" prop="amount">
|
||||||
|
<div class="unit-input-row">
|
||||||
|
<el-input-number
|
||||||
|
v-model="refundForm.amount"
|
||||||
|
:min="0"
|
||||||
|
:max="Number(formatYuan(refundOrderInfo.price))"
|
||||||
|
:precision="2"
|
||||||
|
:step="0.01"
|
||||||
|
style="flex: 1"
|
||||||
|
/>
|
||||||
|
<span class="unit-text">元</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-tip">金额为 0 时退订单全额</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-if="refundForm.refund_method === 'balance'" label="余额类型">
|
||||||
|
<el-select v-model="refundForm.balance_type" style="width: 100%">
|
||||||
|
<el-option label="云币" value="general" />
|
||||||
|
<el-option label="云钻" value="entity" />
|
||||||
|
<el-option label="云点" value="withdraw" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="退款原因">
|
||||||
|
<el-input
|
||||||
|
v-model="refundForm.reason"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
maxlength="120"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请输入退款原因"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="wizard-footer">
|
||||||
|
<el-button @click="refundDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="refundSubmitLoading" @click="submitRefund">确认退款</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 用户选择器 -->
|
<!-- 用户选择器 -->
|
||||||
<UserListSelector
|
<UserListSelector
|
||||||
v-model="userSelectorVisible"
|
v-model="userSelectorVisible"
|
||||||
@@ -603,7 +725,7 @@ import { ref, reactive, onMounted, onActivated, watch } from 'vue'
|
|||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Plus, Delete, Search, Download, Refresh, User, ShoppingCart, Ticket, Money, Close, Setting, Loading, Lock, Document, ArrowRight, Goods, Box } from '@element-plus/icons-vue'
|
import { Plus, Delete, Search, Download, Refresh, User, ShoppingCart, Ticket, Money, Close, Setting, Loading, Lock, Document, ArrowRight, Goods, Box } from '@element-plus/icons-vue'
|
||||||
import { getOrderList, getOrderDetail, createOrder, updateOrder, deleteOrder, retryOrderHook } from '@/api/admin/order'
|
import { getOrderList, getOrderDetail, createOrder, updateOrder, deleteOrder, retryOrderHook, refundOrder } from '@/api/admin/order'
|
||||||
import { getProductPlanList, getProductParameterList, getProductParameterDetail } from '@/api/admin/product'
|
import { getProductPlanList, getProductParameterList, getProductParameterDetail } from '@/api/admin/product'
|
||||||
import UserListSelector from '@/components/admin/UserListSelector.vue'
|
import UserListSelector from '@/components/admin/UserListSelector.vue'
|
||||||
import ProductSelector from '@/components/admin/ProductSelector.vue'
|
import ProductSelector from '@/components/admin/ProductSelector.vue'
|
||||||
@@ -688,6 +810,10 @@ const selectedRows = ref([])
|
|||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const detailDialogVisible = ref(false)
|
const detailDialogVisible = ref(false)
|
||||||
const dialogType = ref('add')
|
const dialogType = ref('add')
|
||||||
|
const refundDialogVisible = ref(false)
|
||||||
|
const refundSubmitLoading = ref(false)
|
||||||
|
const refundFormRef = ref(null)
|
||||||
|
const refundOrderInfo = ref(null)
|
||||||
|
|
||||||
// 选择器弹窗状态
|
// 选择器弹窗状态
|
||||||
const userSelectorVisible = ref(false)
|
const userSelectorVisible = ref(false)
|
||||||
@@ -701,6 +827,34 @@ const selectedProductInfo = ref(null)
|
|||||||
const selectedDiscountCodeInfo = ref(null)
|
const selectedDiscountCodeInfo = ref(null)
|
||||||
const selectedVoucherInfo = ref(null)
|
const selectedVoucherInfo = ref(null)
|
||||||
|
|
||||||
|
// 退款表单
|
||||||
|
const refundForm = reactive({
|
||||||
|
order_id: undefined,
|
||||||
|
refund_method: 'balance',
|
||||||
|
amount: 0,
|
||||||
|
reason: '订单退款',
|
||||||
|
balance_type: 'general'
|
||||||
|
})
|
||||||
|
|
||||||
|
const validateRefundAmount = (rule, value, callback) => {
|
||||||
|
const priceYuan = (Number(refundOrderInfo.value?.price) || 0) / 100
|
||||||
|
const amount = Number(value) || 0
|
||||||
|
if (amount < 0) {
|
||||||
|
callback(new Error('退款金额不能小于 0'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (priceYuan > 0 && amount > priceYuan) {
|
||||||
|
callback(new Error('退款金额不能超过订单金额'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
|
||||||
|
const refundRules = {
|
||||||
|
refund_method: [{ required: true, message: '请选择退款方式', trigger: 'change' }],
|
||||||
|
amount: [{ validator: validateRefundAmount, trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
// 获取订单列表
|
// 获取订单列表
|
||||||
const fetchOrderList = async () => {
|
const fetchOrderList = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -765,10 +919,49 @@ const getTypeText = (type) => {
|
|||||||
|
|
||||||
// 获取支付方式文本
|
// 获取支付方式文本
|
||||||
const getPayTypeText = (payType) => {
|
const getPayTypeText = (payType) => {
|
||||||
const map = { ali: '支付宝', wx: '微信', default: '余额/默认' }
|
const map = {
|
||||||
|
default: '默认/余额',
|
||||||
|
balance: '余额',
|
||||||
|
'007pay': '007pay',
|
||||||
|
admin: '管理员',
|
||||||
|
ali: '支付宝',
|
||||||
|
wx: '微信'
|
||||||
|
}
|
||||||
return map[payType] || payType || '-'
|
return map[payType] || payType || '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取退款状态文本
|
||||||
|
const getRefundStatusText = (status) => {
|
||||||
|
const map = { pending: '退款中', success: '已退款', failed: '退款失败' }
|
||||||
|
return map[status] || status || '未退款'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取退款状态标签类型
|
||||||
|
const getRefundStatusType = (status) => {
|
||||||
|
const map = { pending: 'warning', success: 'success', failed: 'danger' }
|
||||||
|
return map[status] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasRefundInfo = (order) => {
|
||||||
|
return Boolean(order?.refundStatus || order?.refundAmount || order?.refundNo || order?.refundReason || order?.refundTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRefundLocked = (order) => {
|
||||||
|
return Boolean(order?.refundTime || order?.refundStatus === 'pending' || order?.refundStatus === 'success')
|
||||||
|
}
|
||||||
|
|
||||||
|
const canOriginalRefund = (order) => {
|
||||||
|
return Boolean(order?.state === 1 && order?.payType === '007pay' && order?.paymentOrderId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const canBalanceRefund = (order) => {
|
||||||
|
return Boolean(order?.state === 1 && order?.table !== 'userBalance')
|
||||||
|
}
|
||||||
|
|
||||||
|
const canRefund = (order) => {
|
||||||
|
return Boolean(order?.state === 1 && !isRefundLocked(order) && (canOriginalRefund(order) || canBalanceRefund(order)))
|
||||||
|
}
|
||||||
|
|
||||||
// 分转元
|
// 分转元
|
||||||
const formatYuan = (fen) => {
|
const formatYuan = (fen) => {
|
||||||
return ((Number(fen) || 0) / 100).toFixed(2)
|
return ((Number(fen) || 0) / 100).toFixed(2)
|
||||||
@@ -1002,6 +1195,80 @@ const handleRetryOrder = (row) => {
|
|||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开退款弹窗
|
||||||
|
const handleOpenRefund = (row) => {
|
||||||
|
if (!canRefund(row)) {
|
||||||
|
ElMessage.warning('当前订单不满足退款条件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const refundMethod = canOriginalRefund(row) ? 'original' : 'balance'
|
||||||
|
refundOrderInfo.value = row
|
||||||
|
Object.assign(refundForm, {
|
||||||
|
order_id: row.id,
|
||||||
|
refund_method: refundMethod,
|
||||||
|
amount: Number(formatYuan(row.price)),
|
||||||
|
reason: '订单退款',
|
||||||
|
balance_type: 'general'
|
||||||
|
})
|
||||||
|
refundDialogVisible.value = true
|
||||||
|
refundFormRef.value?.clearValidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交退款
|
||||||
|
const submitRefund = async () => {
|
||||||
|
const formEl = refundFormRef.value
|
||||||
|
if (formEl) {
|
||||||
|
const valid = await formEl.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
}
|
||||||
|
|
||||||
|
const methodText = refundForm.refund_method === 'original' ? '原路退款' : '退到余额'
|
||||||
|
const amountText = Number(refundForm.amount) > 0 ? `¥${Number(refundForm.amount).toFixed(2)}` : '订单全额'
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确认将订单「${refundOrderInfo.value?.name || refundForm.order_id}」${methodText} ${amountText} 吗?`,
|
||||||
|
'订单退款',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确认退款',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
refundSubmitLoading.value = true
|
||||||
|
try {
|
||||||
|
const submitData = {
|
||||||
|
order_id: Number(refundForm.order_id),
|
||||||
|
refund_method: refundForm.refund_method,
|
||||||
|
amount: Number(refundForm.amount) || 0,
|
||||||
|
reason: refundForm.reason || '订单退款'
|
||||||
|
}
|
||||||
|
if (refundForm.refund_method === 'balance') {
|
||||||
|
submitData.balance_type = refundForm.balance_type || 'general'
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await refundOrder(submitData)
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
ElMessage.success('退款成功')
|
||||||
|
refundDialogVisible.value = false
|
||||||
|
await fetchOrderList()
|
||||||
|
if (detailDialogVisible.value && orderDetail.value?.id === submitData.order_id) {
|
||||||
|
await handleView(submitData.order_id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.data.message || '退款失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('订单退款失败:', error)
|
||||||
|
ElMessage.error(error.response?.data?.message || '订单退款失败')
|
||||||
|
} finally {
|
||||||
|
refundSubmitLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 删除订单
|
// 删除订单
|
||||||
const handleDelete = (row) => {
|
const handleDelete = (row) => {
|
||||||
ElMessageBox.confirm(`确认删除订单 ${row.name} 吗?`, '警告', {
|
ElMessageBox.confirm(`确认删除订单 ${row.name} 吗?`, '警告', {
|
||||||
@@ -1696,6 +1963,9 @@ watch(() => route.query.order_id, (newId) => {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
.od-section-action {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
.od-section-link .el-icon {
|
.od-section-link .el-icon {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
@@ -1707,4 +1977,48 @@ watch(() => route.query.order_id, (newId) => {
|
|||||||
.od-args {
|
.od-args {
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.refund-dialog-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refund-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: #f7f8fa;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refund-summary-main {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refund-order-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refund-order-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 6px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refund-order-amount {
|
||||||
|
color: #f56c6c;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,346 @@
|
|||||||
|
<template>
|
||||||
|
<div class="payment-config-page">
|
||||||
|
<div class="page-toolbar">
|
||||||
|
<div class="title-block">
|
||||||
|
<div class="title-icon">
|
||||||
|
<el-icon><Money /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>007pay 支付配置</h2>
|
||||||
|
<p>管理 007pay 收银台、回调与支付通道参数</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-actions">
|
||||||
|
<el-button :loading="loading" @click="fetchConfig">
|
||||||
|
<el-icon><Refresh /></el-icon>刷新
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="submitForm">
|
||||||
|
<el-icon><Check /></el-icon>保存配置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="summary-grid">
|
||||||
|
<div class="summary-item">
|
||||||
|
<div class="summary-label">配置状态</div>
|
||||||
|
<div class="summary-value">
|
||||||
|
<el-tag :type="isConfigured ? 'success' : 'warning'" effect="plain">
|
||||||
|
{{ isConfigured ? '已配置' : '待完善' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<div class="summary-label">商户 PID</div>
|
||||||
|
<div class="summary-value mono">{{ form.pid || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<div class="summary-label">通道类型</div>
|
||||||
|
<div class="summary-value">
|
||||||
|
<el-tag type="info" effect="plain">{{ getChannelText(form.channel_type) }}</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-panel" v-loading="loading">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="form"
|
||||||
|
:rules="rules"
|
||||||
|
label-width="150px"
|
||||||
|
class="config-form"
|
||||||
|
>
|
||||||
|
<el-form-item label="API 地址" prop="base_url">
|
||||||
|
<el-input v-model.trim="form.base_url" placeholder="https://pay.example.com" clearable>
|
||||||
|
<template #prefix><el-icon><Connection /></el-icon></template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="商户 PID" prop="pid">
|
||||||
|
<el-input v-model.trim="form.pid" placeholder="请输入商户 PID" clearable>
|
||||||
|
<template #prefix><el-icon><Setting /></el-icon></template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="商户密钥" prop="key">
|
||||||
|
<el-input v-model.trim="form.key" placeholder="请输入商户密钥" show-password clearable>
|
||||||
|
<template #prefix><el-icon><Key /></el-icon></template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="异步回调地址" prop="notify_url">
|
||||||
|
<el-input v-model.trim="form.notify_url" placeholder="https://api.example.com/api/v1/callback/007pay/pay" clearable>
|
||||||
|
<template #prefix><el-icon><Connection /></el-icon></template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="同步跳转地址" prop="return_url">
|
||||||
|
<el-input v-model.trim="form.return_url" placeholder="https://example.com/pay/return" clearable>
|
||||||
|
<template #prefix><el-icon><Connection /></el-icon></template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="通道类型" prop="channel_type">
|
||||||
|
<el-select
|
||||||
|
v-model="form.channel_type"
|
||||||
|
placeholder="请选择或输入通道类型"
|
||||||
|
filterable
|
||||||
|
allow-create
|
||||||
|
default-first-option
|
||||||
|
clearable
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option label="支付宝" value="alipay" />
|
||||||
|
<el-option label="微信支付" value="wxpay" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div class="panel-footer">
|
||||||
|
<el-button @click="resetForm">重置</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="submitForm">保存配置</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Check, Connection, Key, Money, Refresh, Setting } from '@element-plus/icons-vue'
|
||||||
|
import { getPay007Config, updatePay007Config } from '@/api/admin/pay007'
|
||||||
|
|
||||||
|
const formRef = ref(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
|
||||||
|
const emptyForm = {
|
||||||
|
base_url: '',
|
||||||
|
pid: '',
|
||||||
|
key: '',
|
||||||
|
notify_url: '',
|
||||||
|
return_url: '',
|
||||||
|
channel_type: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = reactive({ ...emptyForm })
|
||||||
|
const originalForm = ref({ ...emptyForm })
|
||||||
|
|
||||||
|
const optionalUrlValidator = (rule, value, callback) => {
|
||||||
|
if (!value) {
|
||||||
|
callback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const url = new URL(value)
|
||||||
|
if (!['http:', 'https:'].includes(url.protocol)) {
|
||||||
|
callback(new Error('请输入 http 或 https 地址'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
} catch {
|
||||||
|
callback(new Error('请输入有效地址'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
base_url: [{ validator: optionalUrlValidator, trigger: 'blur' }],
|
||||||
|
notify_url: [{ validator: optionalUrlValidator, trigger: 'blur' }],
|
||||||
|
return_url: [{ validator: optionalUrlValidator, trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const isConfigured = computed(() => {
|
||||||
|
return Boolean(form.base_url && form.pid && form.key && form.notify_url)
|
||||||
|
})
|
||||||
|
|
||||||
|
const getChannelText = (channel) => {
|
||||||
|
const map = {
|
||||||
|
alipay: '支付宝',
|
||||||
|
wxpay: '微信支付'
|
||||||
|
}
|
||||||
|
return map[channel] || channel || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
const assignForm = (data = {}) => {
|
||||||
|
Object.assign(form, {
|
||||||
|
base_url: data.base_url || '',
|
||||||
|
pid: data.pid || '',
|
||||||
|
key: data.key || '',
|
||||||
|
notify_url: data.notify_url || '',
|
||||||
|
return_url: data.return_url || '',
|
||||||
|
channel_type: data.channel_type || ''
|
||||||
|
})
|
||||||
|
originalForm.value = { ...form }
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchConfig = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getPay007Config()
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
assignForm(res.data.data || {})
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.data.message || '获取支付配置失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取 007pay 支付配置失败:', error)
|
||||||
|
ElMessage.error(error.response?.data?.message || '获取支付配置失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
const formEl = formRef.value
|
||||||
|
if (formEl) {
|
||||||
|
const valid = await formEl.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
}
|
||||||
|
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const res = await updatePay007Config({ ...form })
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
assignForm(res.data.data || form)
|
||||||
|
ElMessage.success('支付配置已保存')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.data.message || '保存支付配置失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存 007pay 支付配置失败:', error)
|
||||||
|
ElMessage.error(error.response?.data?.message || '保存支付配置失败')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
Object.assign(form, originalForm.value)
|
||||||
|
formRef.value?.clearValidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(fetchConfig)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.payment-config-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-block {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-icon {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #f0f9eb;
|
||||||
|
color: #67c23a;
|
||||||
|
font-size: 22px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-block h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #1f2d3d;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-block p {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item {
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-label {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-value {
|
||||||
|
min-height: 24px;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-value.mono {
|
||||||
|
font-family: Consolas, Monaco, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel {
|
||||||
|
padding: 24px 22px 18px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-form {
|
||||||
|
max-width: 880px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
padding-top: 18px;
|
||||||
|
border-top: 1px solid #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.page-toolbar {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -213,6 +213,17 @@
|
|||||||
<el-tag :type="getOrderStatusType(row.state)" size="small">{{ getOrderStatusText(row.state) }}</el-tag>
|
<el-tag :type="getOrderStatusType(row.state)" size="small">{{ getOrderStatusText(row.state) }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="支付方式" width="100">
|
||||||
|
<template #default="{row}">{{ getOrderPayTypeText(row.payType) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="退款" width="95">
|
||||||
|
<template #default="{row}">
|
||||||
|
<el-tag v-if="row.refundStatus" :type="getOrderRefundStatusType(row.refundStatus)" size="small">
|
||||||
|
{{ getOrderRefundStatusText(row.refundStatus) }}
|
||||||
|
</el-tag>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="到期时间" width="160">
|
<el-table-column label="到期时间" width="160">
|
||||||
<template #default="{row}">{{ row.expireTime ? formatDate(row.expireTime) : '-' }}</template>
|
<template #default="{row}">{{ row.expireTime ? formatDate(row.expireTime) : '-' }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -1463,26 +1474,48 @@ const fetchUserBalance = async () => {
|
|||||||
|
|
||||||
// 订单状态
|
// 订单状态
|
||||||
const getOrderStatusText = (state) => {
|
const getOrderStatusText = (state) => {
|
||||||
const map = { 0: '待支付', 1: '已支付', 2: '已取消', 3: '已退款', 4: '已完成' }
|
const map = { 0: '待支付', 1: '已支付', 2: '已失效' }
|
||||||
return map[state] || '未知'
|
return map[state] || '未知'
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOrderStatusType = (state) => {
|
const getOrderStatusType = (state) => {
|
||||||
const map = { 0: 'warning', 1: 'success', 2: 'info', 3: 'danger', 4: 'success' }
|
const map = { 0: 'warning', 1: 'success', 2: 'info' }
|
||||||
return map[state] || 'info'
|
return map[state] || 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 订单类型
|
// 订单类型
|
||||||
const getOrderTypeText = (type) => {
|
const getOrderTypeText = (type) => {
|
||||||
const map = { create: '新购', renew: '续费', upgrade: '升级' }
|
const map = { create: '新购', renew: '续费', update: '升级', snapshot: '快照', backup: '备份', data_volume: '数据盘', ipv4: 'IPv4', ipv6: 'IPv6', traffic: '流量', rebuild_image_diff: '重装差价' }
|
||||||
return map[type] || type || '未知'
|
return map[type] || type || '未知'
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOrderTypeTag = (type) => {
|
const getOrderTypeTag = (type) => {
|
||||||
const map = { create: 'primary', renew: 'success', upgrade: 'warning' }
|
const map = { create: 'primary', renew: 'success', update: 'warning' }
|
||||||
return map[type] || 'info'
|
return map[type] || 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getOrderPayTypeText = (payType) => {
|
||||||
|
const map = {
|
||||||
|
default: '默认/余额',
|
||||||
|
balance: '余额',
|
||||||
|
'007pay': '007pay',
|
||||||
|
admin: '管理员',
|
||||||
|
ali: '支付宝',
|
||||||
|
wx: '微信'
|
||||||
|
}
|
||||||
|
return map[payType] || payType || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOrderRefundStatusText = (status) => {
|
||||||
|
const map = { pending: '退款中', success: '已退款', failed: '退款失败' }
|
||||||
|
return map[status] || status || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOrderRefundStatusType = (status) => {
|
||||||
|
const map = { pending: 'warning', success: 'success', failed: 'danger' }
|
||||||
|
return map[status] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
// 工单状态
|
// 工单状态
|
||||||
const getTicketStatusText = (status) => {
|
const getTicketStatusText = (status) => {
|
||||||
const map = { 0: '待处理', 1: '处理中', 2: '已回复', 3: '已关闭' }
|
const map = { 0: '待处理', 1: '处理中', 2: '已回复', 3: '已关闭' }
|
||||||
|
|||||||
Reference in New Issue
Block a user