微信支付配置,赛事bug修复,取消报名退款
This commit is contained in:
parent
150c7f26b9
commit
2bd38e627a
@ -209,7 +209,7 @@ const openDialog = async (id) => {
|
||||
materialRemark: d.materialRemark,
|
||||
materialUploadTime: [d.materialUploadStartTime, d.materialUploadEndTime],
|
||||
matchTime: [d.matchStartTime, d.matchEndTime],
|
||||
regionIds: [d.provinceId, d.cityId, d.districtId].filter((x) => x !== undefined && x !== null),
|
||||
regionIds: [d.provinceCode, d.cityCode, d.districtCode].filter((x) => x !== undefined && x !== null),
|
||||
detailAddress: d.detailAddress,
|
||||
eventIntroduction: d.eventIntroduction,
|
||||
customConfigJson: d.customConfigJson,
|
||||
|
||||
@ -236,7 +236,7 @@ const openDialog = async (id) => {
|
||||
materialRemark: d.materialRemark,
|
||||
materialUploadTime: [d.materialUploadStartTime, d.materialUploadEndTime],
|
||||
matchTime: [d.matchStartTime, d.matchEndTime],
|
||||
regionIds: [d.provinceId, d.cityId, d.districtId].filter((x) => x !== undefined && x !== null),
|
||||
regionIds: [d.provinceCode, d.cityCode, d.districtCode].filter((x) => x !== undefined && x !== null),
|
||||
detailAddress: d.detailAddress,
|
||||
eventIntroduction: d.eventIntroduction,
|
||||
customConfigJson: d.customConfigJson,
|
||||
|
||||
@ -12,10 +12,13 @@
|
||||
{{ scope.row.paymentMethod }}
|
||||
</template>
|
||||
<template #orderType="scope">
|
||||
{{ scope.row.paymentMethod }}
|
||||
{{ scope.row.orderType }}
|
||||
</template>
|
||||
<template #registrationMode="scope">
|
||||
{{ scope.row.paymentMethod }}
|
||||
{{ scope.row.registrationMode }}
|
||||
</template>
|
||||
<template #orderStatus="scope">
|
||||
{{ scope.row.orderStatusText || scope.row.orderStatus }}
|
||||
</template>
|
||||
<template #action="scope">
|
||||
<el-button size="small" text type="primary" @click="onOpenMenu(scope.row.id)">
|
||||
@ -86,7 +89,7 @@ const columns = ref([
|
||||
{ width: 120, prop: 'originalAmount', label: '原金额' },
|
||||
{ width: 120, prop: 'discountAmount', label: '优惠金额' },
|
||||
{ width: 120, prop: 'actualAmount', label: '实付金额' },
|
||||
{ width: 120, prop: 'orderStatus', label: '订单状态' },
|
||||
{ width: 120, prop: 'orderStatus', label: '订单状态', search: { el: 'select', }, enum: [{ label: '全部', value: '' }, { label: '待支付', value: 0 }, { label: '已支付', value: 1 }, { label: '已取消', value: 2 }, { label: '已退款', value: 3 }, { label: '已发货', value: 4 }, { label: '已完成', value: 5 },], },
|
||||
{ width: 120, prop: 'createdTime', label: '生成时间', search: { el: 'date-picker', props: { type: 'daterange' } } },
|
||||
{ prop: 'action', label: '操作', align: 'center', fixed: 'right', width: 120, },
|
||||
]);
|
||||
|
||||
@ -87,8 +87,25 @@
|
||||
</el-form-item>
|
||||
|
||||
<!-- 微信支付商户私钥 -->
|
||||
<el-form-item label="商户私钥(PEM格式)" prop="weChatPayConfig.merchantCertificatePrivateKey">
|
||||
<el-input v-model="formData.weChatPayConfig.merchantCertificatePrivateKey" type="textarea" :rows="4" placeholder="请输入微信支付商户私钥(PEM格式)"></el-input>
|
||||
<el-form-item label="商户私钥文件" prop="weChatPayConfig.merchantCertificatePrivateKey">
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
action="/api/file/upload"
|
||||
:on-success="handlePrivateKeySuccess"
|
||||
:before-upload="beforePrivateKeyUpload"
|
||||
:show-file-list="false"
|
||||
accept=".pem"
|
||||
>
|
||||
<el-button type="primary">点击上传</el-button>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
请上传 .pem 格式的文件
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<div v-if="formData.weChatPayConfig.merchantCertificatePrivateKey" class="private-key-file">
|
||||
已上传: {{ getFileName(formData.weChatPayConfig.merchantCertificatePrivateKey) }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
</el-form>
|
||||
@ -226,6 +243,23 @@ const beforeAvatarUpload = () => {
|
||||
return true
|
||||
}
|
||||
|
||||
const handlePrivateKeySuccess = (response, uploadFile) => {
|
||||
formData.value.weChatPayConfig.merchantCertificatePrivateKey = response[0].url;
|
||||
}
|
||||
|
||||
const beforePrivateKeyUpload = (file) => {
|
||||
const isPem = file.name.endsWith('.pem');
|
||||
if (!isPem) {
|
||||
ElMessage.error('只能上传 .pem 格式的文件');
|
||||
}
|
||||
return isPem;
|
||||
}
|
||||
|
||||
const getFileName = (url) => {
|
||||
if (!url) return '';
|
||||
return url.substring(url.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
||||
// 表单提交
|
||||
const onSubmit = () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
@ -210,30 +210,15 @@
|
||||
|
||||
<!-- 项目和队员列表 -->
|
||||
<div class="project-list">
|
||||
<!-- 示例项目1 - 个人项目 -->
|
||||
<div class="project-card">
|
||||
<!-- 使用真实数据渲染项目 -->
|
||||
<div
|
||||
v-for="project in cancelPreviewData?.projects || []"
|
||||
:key="project.projectId"
|
||||
class="project-card">
|
||||
<div class="project-header">
|
||||
<span class="project-tag">(个人/1) 30秒单摇-少年儿童组/18-20/男</span>
|
||||
</div>
|
||||
<div class="member-group">
|
||||
<div
|
||||
v-for="member in mockMembers1"
|
||||
:key="member.id"
|
||||
:class="['member-tag', { 'member-selected': selectedMembers.includes(member.id) }]"
|
||||
@click="toggleMember(member.id)">
|
||||
{{ member.name }}
|
||||
<el-icon v-if="selectedMembers.includes(member.id)" class="remove-icon" @click.stop="removeMember(member.id)">
|
||||
<Close />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 示例项目2 - 团队项目 -->
|
||||
<div class="project-card">
|
||||
<div class="project-header">
|
||||
<span class="project-tag">(团队/3) 交互绳-少年儿童组/10-24/女</span>
|
||||
<span class="project-tag">({{ project.projectType }}/{{ project.personCount }}) {{ project.projectName }}</span>
|
||||
<el-icon
|
||||
v-if="project.projectType === '团队'"
|
||||
class="remove-team-icon"
|
||||
:class="{ 'team-removed': teamRemoved }"
|
||||
@click="toggleTeam()">
|
||||
@ -242,18 +227,31 @@
|
||||
</div>
|
||||
<div class="member-group">
|
||||
<div
|
||||
v-for="member in mockMembers2"
|
||||
:key="member.id"
|
||||
:class="['member-tag', { 'team-removed': teamRemoved, 'disabled': true }]">
|
||||
{{ member.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="member-group" v-if="mockMembers2.length > 2">
|
||||
<div
|
||||
v-for="member in mockMembers3"
|
||||
:key="member.id"
|
||||
:class="['member-tag', { 'team-removed': teamRemoved, 'disabled': true }]">
|
||||
v-for="member in cancelPreviewData?.members || []"
|
||||
:key="member.memberId"
|
||||
:class="[
|
||||
'member-tag',
|
||||
{
|
||||
'member-selected': selectedMembers.includes(member.memberId),
|
||||
'team-removed': project.projectType === '团队' && teamRemoved,
|
||||
'disabled': project.projectType === '团队'
|
||||
}
|
||||
]"
|
||||
@click="toggleMember(member.memberId)">
|
||||
{{ member.name }}
|
||||
<el-tag
|
||||
v-if="member.isLeader"
|
||||
size="small"
|
||||
type="success"
|
||||
style="margin-left: 4px; font-size: 10px;">
|
||||
队长
|
||||
</el-tag>
|
||||
<el-icon
|
||||
v-if="project.projectType === '个人' && selectedMembers.includes(member.memberId)"
|
||||
class="remove-icon"
|
||||
@click.stop="removeMember(member.memberId)">
|
||||
<Close />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -668,43 +666,14 @@ const hasLimitError = ref(false);
|
||||
const hasCountError = ref(false);
|
||||
const isFreeProject = ref(false); // 控制是否为免费项目
|
||||
|
||||
// 每个项目的状态(根据项目标签判断类型)
|
||||
const isProjectTeamType = (projectTag) => {
|
||||
// 判断是否为团队项目:标签包含"团队"
|
||||
return projectTag && projectTag.includes('团队');
|
||||
};
|
||||
|
||||
const totalRefundAmount = computed(() => {
|
||||
// 免费项目退款金额为0
|
||||
if (isFreeProject.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 示例:每个队员100元
|
||||
const memberPrice = 100;
|
||||
let total = 0;
|
||||
|
||||
if (cancelAll.value) {
|
||||
// 取消全部:所有个人项目队员 + 整个团队
|
||||
// 个人项目队员
|
||||
total += mockMembers1.value.length * memberPrice;
|
||||
// 团队项目(整个团队)
|
||||
total += (mockMembers2.value.length + mockMembers3.value.length) * memberPrice;
|
||||
} else {
|
||||
// 个人项目:按选中的队员计算(只计算个人项目的队员,不包括团队项目)
|
||||
// 个人项目队员ID:1, 2
|
||||
const personalMemberIds = [1, 2];
|
||||
const selectedPersonalMembers = selectedMembers.value.filter(id => personalMemberIds.includes(id));
|
||||
total += selectedPersonalMembers.length * memberPrice;
|
||||
|
||||
// 团队项目:如果团队被取消,则计算整个团队
|
||||
if (teamRemoved.value) {
|
||||
total += (mockMembers2.value.length + mockMembers3.value.length) * memberPrice;
|
||||
}
|
||||
}
|
||||
|
||||
// 边界条件:确保金额不小于0
|
||||
return Math.max(0, total);
|
||||
// 使用后端返回的可退款金额
|
||||
return cancelPreviewData.value?.refundableAmount ?? 0;
|
||||
});
|
||||
|
||||
const toggleMember = (memberId) => {
|
||||
@ -714,7 +683,10 @@ const toggleMember = (memberId) => {
|
||||
}
|
||||
|
||||
// 团队项目队员不允许手动选择
|
||||
const teamMemberIds = [3, 4, 5, 6];
|
||||
const teamMemberIds = (cancelPreviewData.value?.projects || [])
|
||||
.filter(p => p.projectType === '团队')
|
||||
.flatMap(p => (cancelPreviewData.value?.members || []).map(m => m.memberId));
|
||||
|
||||
if (teamMemberIds.includes(memberId)) {
|
||||
return;
|
||||
}
|
||||
@ -743,7 +715,9 @@ const toggleTeam = () => {
|
||||
teamRemoved.value = !teamRemoved.value;
|
||||
if (teamRemoved.value) {
|
||||
// 团队被移除时,清空该团队的队员选择
|
||||
const teamMemberIds = [...mockMembers2.value, ...mockMembers3.value].map(m => m.id);
|
||||
const teamMemberIds = (cancelPreviewData.value?.members || [])
|
||||
.filter(m => !m.isLeader)
|
||||
.map(m => m.memberId);
|
||||
selectedMembers.value = selectedMembers.value.filter(id => !teamMemberIds.includes(id));
|
||||
}
|
||||
};
|
||||
@ -751,8 +725,8 @@ const toggleTeam = () => {
|
||||
// 监听取消全部的变化
|
||||
watch(cancelAll, (newVal) => {
|
||||
if (newVal) {
|
||||
// 取消全部:选中所有个人项目队员,并取消团队
|
||||
selectedMembers.value = [...mockMembers1.value.map(m => m.id)];
|
||||
// 取消全部:选中所有队员,并取消团队
|
||||
selectedMembers.value = (cancelPreviewData.value?.members || []).map(m => m.memberId);
|
||||
teamRemoved.value = true;
|
||||
} else {
|
||||
// 取消取消全部:清空选择
|
||||
@ -777,16 +751,18 @@ const openCancelDialog = async (row) => {
|
||||
isFreeProject.value = false; // 默认为收费项目
|
||||
|
||||
try {
|
||||
const { succeeded, data } = await usereventrecordCancelPreviewGet(row.registrationId);
|
||||
if (succeeded) {
|
||||
const res = await usereventrecordCancelPreviewGet(row.registrationId);
|
||||
const data = res.data || res;
|
||||
if (data) {
|
||||
cancelPreviewData.value = data;
|
||||
// 根据后端返回的数据设置是否为免费项目
|
||||
isFreeProject.value = data?.chargeMode === 0 || data?.actualPaidAmount <= 0;
|
||||
} else {
|
||||
ElMessage.error(data ?? '获取取消报名预览失败');
|
||||
ElMessage.error('获取取消报名预览失败');
|
||||
isShowCancelDialog.value = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ElMessage.error('获取取消报名预览失败');
|
||||
isShowCancelDialog.value = false;
|
||||
} finally {
|
||||
@ -809,12 +785,15 @@ const submitCancel = async () => {
|
||||
payload.personIds = selectedMembers.value;
|
||||
}
|
||||
|
||||
const { succeeded, data } = await usereventrecordCancelExecutePost(payload);
|
||||
const res = await usereventrecordCancelExecutePost(payload);
|
||||
const data = res.data || res;
|
||||
const succeeded = res.succeeded ?? (data?.success ?? true);
|
||||
|
||||
isCancelConfirmed.value = true;
|
||||
cancelResult.value = {
|
||||
success: succeeded,
|
||||
message: data?.message || (succeeded ? '操作成功' : '操作失败'),
|
||||
refundAmount: data?.refundAmount || totalRefundAmount.value,
|
||||
refundAmount: data?.refundAmount ?? totalRefundAmount.value,
|
||||
refundNo: data?.refundNo || null,
|
||||
cancelTime: data?.cancelTime || new Date().toISOString()
|
||||
};
|
||||
@ -824,6 +803,7 @@ const submitCancel = async () => {
|
||||
isShowDialog.value = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ElMessage.error('取消报名失败');
|
||||
isCancelConfirmed.value = true;
|
||||
cancelResult.value = {
|
||||
|
||||
@ -44,47 +44,18 @@
|
||||
</ProTable>
|
||||
|
||||
<!-- 赛事详情弹窗 -->
|
||||
<el-dialog title="赛事详情" v-model="isShowDetailDialog" width="80%" @closed="clearDetailState">
|
||||
<el-dialog title="赛事详情" v-model="isShowDetailDialog" width="80%" style="min-height: 600px;" @closed="clearDetailState">
|
||||
<el-skeleton :rows="12" animated v-if="detailLoading" />
|
||||
<template v-else>
|
||||
<div v-if="detailData" style="display: flex; gap: 18px; align-items: flex-start;">
|
||||
<el-image v-if="detailData?.coverImage" style="width: 180px; height: 120px; flex: 0 0 auto;"
|
||||
:src="detailData.coverImage" fit="cover" />
|
||||
<el-empty v-else description="暂无封面" style="width: 180px; height: 120px;" />
|
||||
|
||||
<div style="flex: 1;">
|
||||
<el-descriptions :column="2" border size="small" v-if="detailData">
|
||||
<el-descriptions-item label="赛事名称">{{ detailData?.name ?? '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="赛事类型">
|
||||
{{ eventTypeText(detailData) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="报名方式">
|
||||
{{ registrationTypeText(detailData) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="收费模式">{{ chargeModeText(detailData) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="赛事状态">
|
||||
<el-tag :type="eventStatusTagType(detailData?.status)">{{ eventStatusText(detailData?.status) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="报名起止时间">
|
||||
{{ formatRange(detailData?.registrationStartTime, detailData?.registrationEndTime) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="比赛起止时间">
|
||||
{{ formatRange(detailData?.matchStartTime, detailData?.matchEndTime) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="积分发放规则">
|
||||
{{ detailData?.pointsRuleName ?? '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="detail-section-title">项目列表</div>
|
||||
<el-table :data="detailProjectsForDisplay" border size="small" style="margin-top: 10px;">
|
||||
<el-table-column prop="projectName" label="项目" align="center" show-overflow-tooltip />
|
||||
<el-table-column prop="groupName" label="组别" align="center" />
|
||||
<el-table-column prop="fee" label="费用(元)" align="center" width="120" />
|
||||
<el-table-column prop="limitCount" label="名额" align="center" width="120" />
|
||||
<el-table-column prop="completionCount" label="完成人数" align="center" width="120" />
|
||||
</el-table>
|
||||
</div>
|
||||
<div v-if="formData">
|
||||
<Step1BasicInfo v-model:modelValue="formData.basicInfo" :readonly="true" />
|
||||
<div style="height:50px;"></div>
|
||||
<Step2Units v-model:modelValue="formData.units" :readonly="true" />
|
||||
<div style="height:50px;"></div>
|
||||
<Step3ProjectConfig v-model:modelValue="formData.projectConfig" :readonly="true" />
|
||||
<div style="height:50px;"></div>
|
||||
<Step4FeeConfig v-model:modelValue="formData.feeConfig" :readonly="true" />
|
||||
<div style="height:100px;"></div>
|
||||
</div>
|
||||
<el-empty v-else description="暂无详情数据" />
|
||||
</template>
|
||||
@ -449,6 +420,10 @@ import {
|
||||
} from '/@/api/match/usereventregistration';
|
||||
import { teamPageGet, teamDetailGet } from '/@/api/match/unitUser';
|
||||
import QRCode from 'qrcodejs2-fixes';
|
||||
import Step1BasicInfo from '/@/views/match/manage/Steps/Step1BasicInfo.vue';
|
||||
import Step2Units from '/@/views/match/manage/Steps/Step2Units.vue';
|
||||
import Step3ProjectConfig from '/@/views/match/manage/Steps/Step3ProjectConfig.vue';
|
||||
import Step4FeeConfig from '/@/views/match/manage/Steps/Step4FeeConfig.vue';
|
||||
|
||||
const tableRef = ref();
|
||||
const initParam = ref({ refresh: true });
|
||||
@ -594,10 +569,87 @@ const detailProjectsForDisplay = computed(() => {
|
||||
const isShowDetailDialog = ref(false);
|
||||
const detailLoading = ref(false);
|
||||
const detailData = ref(null);
|
||||
const formData = ref({
|
||||
// 步骤1:基本信息
|
||||
basicInfo: {
|
||||
id: undefined,
|
||||
eventName: '',
|
||||
coverImage: '',
|
||||
eventVideo: '',
|
||||
eventType: '',
|
||||
registrationType: '',
|
||||
pointsRuleId: undefined,
|
||||
registrationTime: [],
|
||||
isNeedMaterial: false,
|
||||
materialRemark: '',
|
||||
materialUploadTime: [],
|
||||
matchTime: [],
|
||||
regionIds: [],
|
||||
detailAddress: '',
|
||||
eventIntroduction: '',
|
||||
customConfigJson: '',
|
||||
},
|
||||
// 步骤2:参与单位
|
||||
units: {
|
||||
units: [],
|
||||
},
|
||||
// 步骤3:项目与限报配置
|
||||
projectConfig: {
|
||||
projects: [],
|
||||
registrationLimit: {
|
||||
individualProjectLimit: 1,
|
||||
teamProjectLimit: 1,
|
||||
},
|
||||
},
|
||||
// 步骤4:费用与账户配置
|
||||
feeConfig: {
|
||||
chargeMode: 'Free',
|
||||
price: 0,
|
||||
merchantId: undefined,
|
||||
merchantName: '',
|
||||
creditCode: '',
|
||||
bankName: '',
|
||||
accountNumber: '',
|
||||
},
|
||||
});
|
||||
|
||||
const clearDetailState = () => {
|
||||
detailLoading.value = false;
|
||||
detailData.value = null;
|
||||
formData.value = {
|
||||
basicInfo: {
|
||||
id: undefined,
|
||||
eventName: '',
|
||||
coverImage: '',
|
||||
eventVideo: '',
|
||||
eventType: '',
|
||||
registrationType: '',
|
||||
pointsRuleId: undefined,
|
||||
registrationTime: [],
|
||||
isNeedMaterial: false,
|
||||
materialRemark: '',
|
||||
materialUploadTime: [],
|
||||
matchTime: [],
|
||||
regionIds: [],
|
||||
detailAddress: '',
|
||||
eventIntroduction: '',
|
||||
customConfigJson: '',
|
||||
},
|
||||
units: { units: [] },
|
||||
projectConfig: {
|
||||
projects: [],
|
||||
registrationLimit: { individualProjectLimit: 1, teamProjectLimit: 1 },
|
||||
},
|
||||
feeConfig: {
|
||||
chargeMode: 'Free',
|
||||
price: 0,
|
||||
merchantId: undefined,
|
||||
merchantName: '',
|
||||
creditCode: '',
|
||||
bankName: '',
|
||||
accountNumber: '',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const openDetailDialog = async (row) => {
|
||||
@ -613,6 +665,34 @@ const openDetailDialog = async (row) => {
|
||||
return;
|
||||
}
|
||||
detailData.value = data ?? null;
|
||||
if (data) {
|
||||
// 映射数据到 formData 结构
|
||||
formData.value.basicInfo = {
|
||||
...formData.value.basicInfo,
|
||||
id: data.id,
|
||||
eventName: data.name || data.eventName || '',
|
||||
coverImage: data.coverImage || '',
|
||||
eventVideo: data.eventVideo || '',
|
||||
eventType: data.type || data.eventType || '',
|
||||
registrationType: data.registrationType || data.registrationMode || '',
|
||||
pointsRuleId: data.pointsRuleId,
|
||||
registrationTime: [data.registrationStartTime, data.registrationEndTime],
|
||||
isNeedMaterial: !!data.isNeedMaterial,
|
||||
materialRemark: data.materialRemark || '',
|
||||
materialUploadTime: [data.materialUploadStartTime, data.materialUploadEndTime],
|
||||
matchTime: [data.matchStartTime, data.matchEndTime],
|
||||
regionIds: [data.provinceId, data.cityId, data.districtId].filter((x) => x !== undefined && x !== null),
|
||||
detailAddress: data.detailAddress || '',
|
||||
eventIntroduction: data.eventIntroduction || '',
|
||||
customConfigJson: data.customConfigJson || '',
|
||||
};
|
||||
formData.value.units = { units: data.units || [] };
|
||||
formData.value.projectConfig = {
|
||||
projects: data.projects || [],
|
||||
registrationLimit: data.registrationLimit || { individualProjectLimit: 1, teamProjectLimit: 1 },
|
||||
};
|
||||
formData.value.feeConfig = { ...formData.value.feeConfig, ...(data.feeConfig || {}) };
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('赛事详情加载失败');
|
||||
} finally {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user