微信支付配置,赛事bug修复,取消报名退款

This commit is contained in:
马梓彭 2026-04-03 09:45:46 +08:00
parent 150c7f26b9
commit 2bd38e627a
6 changed files with 216 additions and 119 deletions

View File

@ -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,

View File

@ -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,

View File

@ -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, },
]);

View File

@ -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;

View File

@ -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 = {

View File

@ -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 {